implement internal links in PDF

This commit is contained in:
Alexander Wang 2023-03-02 22:07:41 -08:00
parent 09544af0e0
commit e6644549be
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
6 changed files with 79 additions and 14 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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

Binary file not shown.

View file

@ -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
}