implement internal links in PDF
This commit is contained in:
parent
09544af0e0
commit
e6644549be
6 changed files with 79 additions and 14 deletions
|
|
@ -1,10 +1,11 @@
|
|||
#### Features 🚀
|
||||
|
||||
- PDF exports support external links [#891](https://github.com/terrastruct/d2/issues/891)
|
||||
- PDF exports support internal links [#891](https://github.com/terrastruct/d2/issues/966)
|
||||
- `border-radius` is now supported on connections (ELK and TALA only, since Dagre uses curves). [#913](https://github.com/terrastruct/d2/pull/913)
|
||||
|
||||
#### Improvements 🧹
|
||||
|
||||
- PDF exports now support external links on shapes [#891](https://github.com/terrastruct/d2/issues/891)
|
||||
- SVGs are fit to top left by default to avoid issues with zooming. [#954](https://github.com/terrastruct/d2/pull/954)
|
||||
- Person shapes now have labels below them and don't need to expand as much. [#960](https://github.com/terrastruct/d2/pull/960)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
||||
"oss.terrastruct.com/d2/lib/imgbundler"
|
||||
ctxlog "oss.terrastruct.com/d2/lib/log"
|
||||
"oss.terrastruct.com/d2/lib/pdf"
|
||||
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
||||
"oss.terrastruct.com/d2/lib/png"
|
||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
||||
|
|
@ -291,7 +292,8 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
|||
|
||||
var svg []byte
|
||||
if filepath.Ext(outputPath) == ".pdf" {
|
||||
svg, err = renderPDF(ctx, ms, plugin, sketch, pad, themeID, outputPath, page, ruler, diagram, nil, nil)
|
||||
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
||||
svg, err = renderPDF(ctx, ms, plugin, sketch, pad, themeID, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
||||
} else {
|
||||
compileDur := time.Since(start)
|
||||
svg, err = render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
||||
|
|
@ -443,7 +445,7 @@ func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketc
|
|||
return svg, nil
|
||||
}
|
||||
|
||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string) (svg []byte, err error) {
|
||||
func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, sketch bool, pad, themeID int64, outputPath string, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram, pdf *pdflib.GoFPDF, boardPath []string, pageMap map[string]int) (svg []byte, err error) {
|
||||
var isRoot bool
|
||||
if pdf == nil {
|
||||
pdf = pdflib.Init()
|
||||
|
|
@ -504,26 +506,26 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
|
|||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY)
|
||||
err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY, pageMap)
|
||||
if err != nil {
|
||||
return svg, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath)
|
||||
_, err := renderPDF(ctx, ms, plugin, sketch, pad, themeID, "", page, ruler, dl, pdf, currBoardPath, pageMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,28 @@ scenarios: {
|
|||
assert.TestdataDir(t, filepath.Join(dir, "life"))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "internal_linked_pdf",
|
||||
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
||||
writeFile(t, dir, "in.d2", `cat: how does the cat go? {
|
||||
link: layers.cat
|
||||
}
|
||||
layers: {
|
||||
cat: {
|
||||
home: {
|
||||
link: _
|
||||
}
|
||||
the cat -> meow: goes
|
||||
}
|
||||
}
|
||||
`)
|
||||
err := runTestMain(t, ctx, dir, env, "in.d2", "out.pdf")
|
||||
assert.Success(t, err)
|
||||
|
||||
pdf := readFile(t, dir, "out.pdf")
|
||||
testdataIgnoreDiff(t, ".pdf", pdf)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
BIN
e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf
vendored
Normal file
BIN
e2etests-cli/testdata/TestCLI_E2E/internal_linked_pdf.exp.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
|
||||
"oss.terrastruct.com/d2/d2parser"
|
||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/d2themes"
|
||||
|
|
@ -58,7 +59,7 @@ func (g *GoFPDF) GetFillRGB(themeID int64, fill string) (color.RGB, error) {
|
|||
return color.Hex2RGB(fill)
|
||||
}
|
||||
|
||||
func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill string, shapes []d2target.Shape, pad int64, viewboxX, viewboxY float64) error {
|
||||
func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill string, shapes []d2target.Shape, pad int64, viewboxX, viewboxY float64, pageMap map[string]int) error {
|
||||
var opt gofpdf.ImageOptions
|
||||
opt.ImageType = "png"
|
||||
imageInfo := g.pdf.RegisterImageOptionsReader(strings.Join(boardPath, "/"), opt, bytes.NewReader(png))
|
||||
|
|
@ -122,14 +123,28 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill
|
|||
imageY := headerHeight + (pageHeight-imageHeight)/2
|
||||
g.pdf.ImageOptions(strings.Join(boardPath, "/"), imageX, imageY, imageWidth, imageHeight, false, opt, 0, "")
|
||||
|
||||
// Draw external links
|
||||
// Draw links
|
||||
for _, shape := range shapes {
|
||||
if shape.Link != "" {
|
||||
linkX := imageX + float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)
|
||||
linkY := imageY + float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)
|
||||
linkWidth := float64(shape.Width) + float64(shape.StrokeWidth*2)
|
||||
linkHeight := float64(shape.Height) + float64(shape.StrokeWidth*2)
|
||||
if shape.Link == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
linkX := imageX + float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)
|
||||
linkY := imageY + float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)
|
||||
linkWidth := float64(shape.Width) + float64(shape.StrokeWidth*2)
|
||||
linkHeight := float64(shape.Height) + float64(shape.StrokeWidth*2)
|
||||
|
||||
key, err := d2parser.ParseKey(shape.Link)
|
||||
if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
|
||||
// External link
|
||||
g.pdf.LinkString(linkX, linkY, linkWidth, linkHeight, shape.Link)
|
||||
} else {
|
||||
// Internal link
|
||||
if pageNum, ok := pageMap[shape.Link]; ok {
|
||||
linkID := g.pdf.AddLink()
|
||||
g.pdf.SetLink(linkID, 0, pageNum+1)
|
||||
g.pdf.Link(linkX, linkY, linkWidth, linkHeight, linkID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,3 +164,28 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill
|
|||
func (g *GoFPDF) Export(outputPath string) error {
|
||||
return g.pdf.OutputFileAndClose(outputPath)
|
||||
}
|
||||
|
||||
// BuildPDFPageMap returns a map from board path to page int
|
||||
// To map correctly, it must follow the same traversal of PDF building
|
||||
func BuildPDFPageMap(diagram *d2target.Diagram, dictionary map[string]int, path []string) map[string]int {
|
||||
newPath := append(path, diagram.Name)
|
||||
if dictionary == nil {
|
||||
dictionary = map[string]int{}
|
||||
newPath[0] = "root"
|
||||
}
|
||||
|
||||
key := strings.Join(newPath, ".")
|
||||
dictionary[key] = len(dictionary)
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
BuildPDFPageMap(dl, dictionary, append(newPath, "layers"))
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
BuildPDFPageMap(dl, dictionary, append(newPath, "scenarios"))
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
BuildPDFPageMap(dl, dictionary, append(newPath, "steps"))
|
||||
}
|
||||
|
||||
return dictionary
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue