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 🚀
|
#### 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)
|
- `border-radius` is now supported on connections (ELK and TALA only, since Dagre uses curves). [#913](https://github.com/terrastruct/d2/pull/913)
|
||||||
|
|
||||||
#### Improvements 🧹
|
#### 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)
|
- 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)
|
- 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/d2themes/d2themescatalog"
|
||||||
"oss.terrastruct.com/d2/lib/imgbundler"
|
"oss.terrastruct.com/d2/lib/imgbundler"
|
||||||
ctxlog "oss.terrastruct.com/d2/lib/log"
|
ctxlog "oss.terrastruct.com/d2/lib/log"
|
||||||
|
"oss.terrastruct.com/d2/lib/pdf"
|
||||||
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
pdflib "oss.terrastruct.com/d2/lib/pdf"
|
||||||
"oss.terrastruct.com/d2/lib/png"
|
"oss.terrastruct.com/d2/lib/png"
|
||||||
"oss.terrastruct.com/d2/lib/textmeasure"
|
"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
|
var svg []byte
|
||||||
if filepath.Ext(outputPath) == ".pdf" {
|
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 {
|
} else {
|
||||||
compileDur := time.Since(start)
|
compileDur := time.Since(start)
|
||||||
svg, err = render(ctx, ms, compileDur, plugin, sketch, pad, themeID, darkThemeID, inputPath, outputPath, bundle, forceAppendix, page, ruler, diagram)
|
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
|
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
|
var isRoot bool
|
||||||
if pdf == nil {
|
if pdf == nil {
|
||||||
pdf = pdflib.Init()
|
pdf = pdflib.Init()
|
||||||
|
|
@ -504,26 +506,26 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return svg, err
|
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 {
|
if err != nil {
|
||||||
return svg, err
|
return svg, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dl := range diagram.Layers {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,28 @@ scenarios: {
|
||||||
assert.TestdataDir(t, filepath.Join(dir, "life"))
|
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()
|
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"
|
"github.com/jung-kurt/gofpdf"
|
||||||
|
|
||||||
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||||
"oss.terrastruct.com/d2/d2target"
|
"oss.terrastruct.com/d2/d2target"
|
||||||
"oss.terrastruct.com/d2/d2themes"
|
"oss.terrastruct.com/d2/d2themes"
|
||||||
|
|
@ -58,7 +59,7 @@ func (g *GoFPDF) GetFillRGB(themeID int64, fill string) (color.RGB, error) {
|
||||||
return color.Hex2RGB(fill)
|
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
|
var opt gofpdf.ImageOptions
|
||||||
opt.ImageType = "png"
|
opt.ImageType = "png"
|
||||||
imageInfo := g.pdf.RegisterImageOptionsReader(strings.Join(boardPath, "/"), opt, bytes.NewReader(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
|
imageY := headerHeight + (pageHeight-imageHeight)/2
|
||||||
g.pdf.ImageOptions(strings.Join(boardPath, "/"), imageX, imageY, imageWidth, imageHeight, false, opt, 0, "")
|
g.pdf.ImageOptions(strings.Join(boardPath, "/"), imageX, imageY, imageWidth, imageHeight, false, opt, 0, "")
|
||||||
|
|
||||||
// Draw external links
|
// Draw links
|
||||||
for _, shape := range shapes {
|
for _, shape := range shapes {
|
||||||
if shape.Link != "" {
|
if shape.Link == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
linkX := imageX + float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)
|
linkX := imageX + float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth)
|
||||||
linkY := imageY + float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)
|
linkY := imageY + float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth)
|
||||||
linkWidth := float64(shape.Width) + float64(shape.StrokeWidth*2)
|
linkWidth := float64(shape.Width) + float64(shape.StrokeWidth*2)
|
||||||
linkHeight := float64(shape.Height) + 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)
|
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 {
|
func (g *GoFPDF) Export(outputPath string) error {
|
||||||
return g.pdf.OutputFileAndClose(outputPath)
|
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