diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index f3c0d2a77..2a1673004 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -2,4 +2,9 @@ #### Improvements 🧹 +- PDF exports now support external links on shapes [#891](https://github.com/terrastruct/d2/issues/891) + #### Bugfixes ⛑️ + +- Fixes a regression where PNG backgrounds could be cut off in the appendix. [#941](https://github.com/terrastruct/d2/pull/941) +- Fixes zooming not working in watch mode. [#944](https://github.com/terrastruct/d2/pull/944) diff --git a/ci/sub b/ci/sub index 519828001..690bc39e5 160000 --- a/ci/sub +++ b/ci/sub @@ -1 +1 @@ -Subproject commit 5198280010adc30aabb611579579916abfe20a45 +Subproject commit 690bc39e545cae76314fa32effe343a088e2a52e diff --git a/d2cli/main.go b/d2cli/main.go index 6d928f7d1..11f534569 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "time" @@ -488,7 +489,16 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, ske return svg, err } - err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill) + viewboxSlice := appendix.FindViewboxSlice(svg) + viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64) + if err != nil { + return svg, err + } + viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64) + if err != nil { + return svg, err + } + err = pdf.AddPDFPage(pngImg, currBoardPath, themeID, rootFill, diagram.Shapes, pad, viewboxX, viewboxY) if err != nil { return svg, err } diff --git a/d2cli/static/watch.css b/d2cli/static/watch.css index e89729739..3e389ddbb 100644 --- a/d2cli/static/watch.css +++ b/d2cli/static/watch.css @@ -1,13 +1,3 @@ -body { - margin: 0; -} - -#d2-svg-container > svg { - position: absolute; - width: 100%; - height: 100%; -} - #d2-err { /* This style was copied from Chrome's svg parser error style. */ white-space: pre-wrap; diff --git a/d2cli/static/watch.js b/d2cli/static/watch.js index 1b21f801f..156a2992d 100644 --- a/d2cli/static/watch.js +++ b/d2cli/static/watch.js @@ -11,6 +11,8 @@ function init(reconnectDelay) { const ws = new WebSocket( `ws://${window.location.host}${window.location.pathname}watch` ); + let isInit = true; + let ratio; ws.onopen = () => { reconnectDelay = 1000; console.info("watch websocket opened"); @@ -31,6 +33,29 @@ function init(reconnectDelay) { // setting innerHTML to only the actual svg innards. However then you also need to parse // out the width, height and viewbox out of the top level SVG tag and update those manually. d2SVG.innerHTML = msg.svg; + + const svgEl = d2SVG.querySelector("#d2-svg"); + // just use inner SVG in watch mode + svgEl.parentElement.replaceWith(svgEl); + let width = parseInt(svgEl.getAttribute("width"), 10); + let height = parseInt(svgEl.getAttribute("height"), 10); + if (isInit) { + if (width > height) { + if (width > window.innerWidth) { + ratio = window.innerWidth / width; + } + } else if (height > window.innerHeight) { + ratio = window.innerHeight / height; + } + // Scale svg fit to zoom + isInit = false; + } + if (ratio) { + // body padding is 8px + svgEl.setAttribute("width", width * ratio - 16); + svgEl.setAttribute("height", height * ratio - 16); + } + d2ErrDiv.style.display = "none"; } if (msg.err) { diff --git a/d2renderers/d2svg/appendix/appendix.go b/d2renderers/d2svg/appendix/appendix.go index 3cb6a48a1..ce882cb59 100644 --- a/d2renderers/d2svg/appendix/appendix.go +++ b/d2renderers/d2svg/appendix/appendix.go @@ -52,6 +52,14 @@ const ( var viewboxRegex = regexp.MustCompile(`viewBox=\"([0-9\- ]+)\"`) var widthRegex = regexp.MustCompile(`width=\"([.0-9]+)\"`) var heightRegex = regexp.MustCompile(`height=\"([.0-9]+)\"`) +var svgRegex = regexp.MustCompile(``) + +func FindViewboxSlice(svg []byte) []string { + viewboxMatches := viewboxRegex.FindAllStringSubmatch(string(svg), 2) + viewboxMatch := viewboxMatches[1] + viewboxRaw := viewboxMatch[1] + return strings.Split(viewboxRaw, " ") +} func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []byte { svg := string(in) @@ -91,14 +99,22 @@ func Append(diagram *d2target.Diagram, ruler *textmeasure.Ruler, in []byte) []by newOuterViewbox := fmt.Sprintf(`viewBox="0 0 %d %d"`, viewboxWidth, viewboxHeight) newViewbox := fmt.Sprintf(`viewBox="%s %s %s %s"`, viewboxSlice[0], viewboxSlice[1], strconv.Itoa(viewboxWidth), strconv.Itoa(viewboxHeight)) - widthMatches := widthRegex.FindAllStringSubmatch(svg, 2) - heightMatches := heightRegex.FindAllStringSubmatch(svg, 2) + dimensionsToUpdate := 2 + outerSVG := svgRegex.FindString(svg) + // if outer svg has dimensions set we also need to update it + if widthRegex.FindString(outerSVG) != "" { + dimensionsToUpdate++ + } + + // update 1st 3 matches of width and height 1st is outer svg (if dimensions are set), 2nd inner svg, 3rd is background color rect + widthMatches := widthRegex.FindAllStringSubmatch(svg, dimensionsToUpdate) + heightMatches := heightRegex.FindAllStringSubmatch(svg, dimensionsToUpdate) newWidth := fmt.Sprintf(`width="%s"`, strconv.Itoa(viewboxWidth)) newHeight := fmt.Sprintf(`height="%s"`, strconv.Itoa(viewboxHeight)) svg = strings.Replace(svg, viewboxMatches[0][0], newOuterViewbox, 1) svg = strings.Replace(svg, viewboxMatch[0], newViewbox, 1) - for i := 0; i < 2; i++ { + for i := 0; i < dimensionsToUpdate; i++ { svg = strings.Replace(svg, widthMatches[i][0], newWidth, 1) svg = strings.Replace(svg, heightMatches[i][0], newHeight, 1) } diff --git a/go.mod b/go.mod index c75007881..34b2a85ea 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 gonum.org/v1/plot v0.12.0 nhooyr.io/websocket v1.8.7 - oss.terrastruct.com/util-go v0.0.0-20230228050345-d1fed4d6be62 + oss.terrastruct.com/util-go v0.0.0-20230301015829-35b30391c74d ) require ( diff --git a/go.sum b/go.sum index 4addedab4..4c1b4fb99 100644 --- a/go.sum +++ b/go.sum @@ -277,6 +277,6 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -oss.terrastruct.com/util-go v0.0.0-20230228050345-d1fed4d6be62 h1:XQZNMkHQr2q1eJIcHcgja29X04oDG8SqqdICCgxe5Bk= -oss.terrastruct.com/util-go v0.0.0-20230228050345-d1fed4d6be62/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU= +oss.terrastruct.com/util-go v0.0.0-20230301015829-35b30391c74d h1:+1Bp2bYA7bieedJuqbiwOLhnMs6GQQLB4sNX7BcDbSQ= +oss.terrastruct.com/util-go v0.0.0-20230301015829-35b30391c74d/go.mod h1:Fwy72FDIOOM4K8F96ScXkxHHppR1CPfUyo9+x9c1PBU= rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= diff --git a/lib/pdf/pdf.go b/lib/pdf/pdf.go index 7cfc9b0b1..f92d9a197 100644 --- a/lib/pdf/pdf.go +++ b/lib/pdf/pdf.go @@ -8,6 +8,7 @@ import ( "github.com/jung-kurt/gofpdf" "oss.terrastruct.com/d2/d2renderers/d2fonts" + "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/d2themes" "oss.terrastruct.com/d2/d2themes/d2themescatalog" "oss.terrastruct.com/d2/lib/color" @@ -19,13 +20,13 @@ type GoFPDF struct { func Init() *GoFPDF { newGofPDF := gofpdf.NewCustom(&gofpdf.InitType{ - UnitStr: "in", + UnitStr: "pt", }) newGofPDF.AddUTF8FontFromBytes("source", "", d2fonts.FontFaces[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_REGULAR)]) newGofPDF.AddUTF8FontFromBytes("source", "B", d2fonts.FontFaces[d2fonts.SourceSansPro.Font(0, d2fonts.FONT_STYLE_BOLD)]) newGofPDF.SetAutoPageBreak(false, 0) - newGofPDF.SetLineWidth(0.05) + newGofPDF.SetLineWidth(2) newGofPDF.SetMargins(0, 0, 0) fpdf := GoFPDF{ @@ -57,7 +58,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) error { +func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill string, shapes []d2target.Shape, pad int64, viewboxX, viewboxY float64) error { var opt gofpdf.ImageOptions opt.ImageType = "png" imageInfo := g.pdf.RegisterImageOptionsReader(strings.Join(boardPath, "/"), opt, bytes.NewReader(png)) @@ -73,10 +74,10 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill g.pdf.SetFont("source", "B", 14) pathString := strings.Join(boardPath, " / ") - headerMargin := 0.3 + headerMargin := 28.0 headerWidth := g.pdf.GetStringWidth(pathString) + 2*headerMargin - minPageDimension := 6.0 + minPageDimension := 576.0 pageWidth = math.Max(math.Max(minPageDimension, imageWidth), headerWidth) pageHeight = math.Max(minPageDimension, imageHeight) @@ -86,7 +87,7 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill } // Add page - headerHeight := 0.75 + headerHeight := 72.0 g.pdf.AddPageFormat("", gofpdf.SizeType{Wd: pageWidth, Ht: pageHeight + headerHeight}) // Draw header @@ -117,17 +118,30 @@ func (g *GoFPDF) AddPDFPage(png []byte, boardPath []string, themeID int64, fill g.pdf.CellFormat(pageWidth-prefixWidth-headerMargin, headerHeight, boardName, "", 0, "", false, 0, "") // Draw image - g.pdf.ImageOptions(strings.Join(boardPath, "/"), (pageWidth-imageWidth)/2, headerHeight+(pageHeight-imageHeight)/2, imageWidth, imageHeight, false, opt, 0, "") + imageX := (pageWidth - imageWidth) / 2 + imageY := headerHeight + (pageHeight-imageHeight)/2 + g.pdf.ImageOptions(strings.Join(boardPath, "/"), imageX, imageY, imageWidth, imageHeight, false, opt, 0, "") + + // Draw external 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) + g.pdf.LinkString(linkX, linkY, linkWidth, linkHeight, shape.Link) + } + } // Draw header/img seperator g.pdf.SetXY(headerMargin, headerHeight) - g.pdf.SetLineWidth(0.01) + g.pdf.SetLineWidth(1) if fillRGB.IsLight() { g.pdf.SetDrawColor(10, 15, 37) // steel-900 } else { g.pdf.SetDrawColor(255, 255, 255) } - g.pdf.CellFormat(pageWidth-(headerMargin*2), 0.01, "", "T", 0, "", false, 0, "") + g.pdf.CellFormat(pageWidth-(headerMargin*2), 1, "", "T", 0, "", false, 0, "") return nil } diff --git a/lib/version/version.go b/lib/version/version.go index a67077ab1..70b2d0474 100644 --- a/lib/version/version.go +++ b/lib/version/version.go @@ -1,4 +1,4 @@ package version // Pre-built binaries will have version set correctly during build time. -var Version = "v0.2.1-HEAD" +var Version = "v0.2.2-HEAD"