2023-02-15 01:28:42 +00:00
|
|
|
package pdf
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"math"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
2023-02-23 23:59:50 +00:00
|
|
|
|
2023-03-03 06:07:41 +00:00
|
|
|
"oss.terrastruct.com/d2/d2parser"
|
2023-02-23 23:50:56 +00:00
|
|
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
2023-02-28 04:02:33 +00:00
|
|
|
"oss.terrastruct.com/d2/d2target"
|
2023-02-28 03:50:30 +00:00
|
|
|
"oss.terrastruct.com/d2/d2themes"
|
|
|
|
|
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
2023-02-27 22:10:59 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/color"
|
2023-02-15 01:28:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type GoFPDF struct {
|
|
|
|
|
pdf *gofpdf.Fpdf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Init() *GoFPDF {
|
|
|
|
|
newGofPDF := gofpdf.NewCustom(&gofpdf.InitType{
|
2023-02-28 04:02:33 +00:00
|
|
|
UnitStr: "pt",
|
2023-02-15 01:28:42 +00:00
|
|
|
})
|
|
|
|
|
|
2023-02-23 23:50:56 +00:00
|
|
|
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)])
|
2023-02-15 01:28:42 +00:00
|
|
|
newGofPDF.SetAutoPageBreak(false, 0)
|
2023-02-28 04:02:33 +00:00
|
|
|
newGofPDF.SetLineWidth(2)
|
2023-02-15 01:28:42 +00:00
|
|
|
newGofPDF.SetMargins(0, 0, 0)
|
|
|
|
|
|
|
|
|
|
fpdf := GoFPDF{
|
|
|
|
|
pdf: newGofPDF,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &fpdf
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 03:50:30 +00:00
|
|
|
func (g *GoFPDF) GetFillRGB(themeID int64, fill string) (color.RGB, error) {
|
2023-02-28 00:05:41 +00:00
|
|
|
if fill == "" || strings.ToLower(fill) == "transparent" {
|
2023-02-27 22:10:59 +00:00
|
|
|
return color.RGB{
|
|
|
|
|
Red: 255,
|
|
|
|
|
Green: 255,
|
|
|
|
|
Blue: 255,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-28 03:50:30 +00:00
|
|
|
if color.IsThemeColor(fill) {
|
|
|
|
|
theme := d2themescatalog.Find(themeID)
|
|
|
|
|
fill = d2themes.ResolveThemeColor(theme, fill)
|
2023-02-28 03:55:37 +00:00
|
|
|
} else {
|
|
|
|
|
rgb := color.Name2RGB(fill)
|
|
|
|
|
if (rgb != color.RGB{}) {
|
|
|
|
|
return rgb, nil
|
|
|
|
|
}
|
2023-02-28 03:50:30 +00:00
|
|
|
}
|
|
|
|
|
|
2023-02-27 22:10:59 +00:00
|
|
|
return color.Hex2RGB(fill)
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 06:07:41 +00:00
|
|
|
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 {
|
2023-02-15 01:28:42 +00:00
|
|
|
var opt gofpdf.ImageOptions
|
|
|
|
|
opt.ImageType = "png"
|
|
|
|
|
imageInfo := g.pdf.RegisterImageOptionsReader(strings.Join(boardPath, "/"), opt, bytes.NewReader(png))
|
|
|
|
|
if g.pdf.Err() {
|
|
|
|
|
return g.pdf.Error()
|
|
|
|
|
}
|
|
|
|
|
imageWidth := imageInfo.Width() / 2
|
|
|
|
|
imageHeight := imageInfo.Height() / 2
|
|
|
|
|
|
|
|
|
|
// calculate page dimensions
|
|
|
|
|
var pageWidth float64
|
|
|
|
|
var pageHeight float64
|
|
|
|
|
|
|
|
|
|
g.pdf.SetFont("source", "B", 14)
|
|
|
|
|
pathString := strings.Join(boardPath, " / ")
|
2023-02-28 04:02:33 +00:00
|
|
|
headerMargin := 28.0
|
2023-02-15 01:28:42 +00:00
|
|
|
headerWidth := g.pdf.GetStringWidth(pathString) + 2*headerMargin
|
|
|
|
|
|
2023-02-28 20:45:00 +00:00
|
|
|
minPageDimension := 576.0
|
2023-02-15 01:28:42 +00:00
|
|
|
pageWidth = math.Max(math.Max(minPageDimension, imageWidth), headerWidth)
|
|
|
|
|
pageHeight = math.Max(minPageDimension, imageHeight)
|
|
|
|
|
|
2023-02-28 03:50:30 +00:00
|
|
|
fillRGB, err := g.GetFillRGB(themeID, fill)
|
2023-02-27 22:10:59 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-15 01:28:42 +00:00
|
|
|
// Add page
|
2023-02-28 04:02:33 +00:00
|
|
|
headerHeight := 72.0
|
2023-02-15 01:28:42 +00:00
|
|
|
g.pdf.AddPageFormat("", gofpdf.SizeType{Wd: pageWidth, Ht: pageHeight + headerHeight})
|
|
|
|
|
|
|
|
|
|
// Draw header
|
2023-02-27 22:10:59 +00:00
|
|
|
g.pdf.SetFillColor(int(fillRGB.Red), int(fillRGB.Green), int(fillRGB.Blue))
|
|
|
|
|
g.pdf.Rect(0, 0, pageWidth, pageHeight+headerHeight, "F")
|
|
|
|
|
if fillRGB.IsLight() {
|
|
|
|
|
g.pdf.SetTextColor(10, 15, 37) // steel-900
|
|
|
|
|
} else {
|
|
|
|
|
g.pdf.SetTextColor(255, 255, 255)
|
|
|
|
|
}
|
2023-02-15 01:28:42 +00:00
|
|
|
g.pdf.SetFont("source", "", 14)
|
|
|
|
|
|
|
|
|
|
// Draw board path prefix
|
|
|
|
|
var prefixWidth float64
|
|
|
|
|
prefixPath := boardPath[:len(boardPath)-1]
|
|
|
|
|
if len(prefixPath) > 0 {
|
|
|
|
|
prefix := strings.Join(boardPath[:len(boardPath)-1], " / ") + " / "
|
|
|
|
|
prefixWidth = g.pdf.GetStringWidth(prefix)
|
|
|
|
|
|
|
|
|
|
g.pdf.SetXY(headerMargin, 0)
|
|
|
|
|
g.pdf.CellFormat(prefixWidth, headerHeight, prefix, "", 0, "", false, 0, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw board name
|
|
|
|
|
boardName := boardPath[len(boardPath)-1]
|
|
|
|
|
g.pdf.SetFont("source", "B", 14)
|
|
|
|
|
g.pdf.SetXY(prefixWidth+headerMargin, 0)
|
|
|
|
|
g.pdf.CellFormat(pageWidth-prefixWidth-headerMargin, headerHeight, boardName, "", 0, "", false, 0, "")
|
|
|
|
|
|
|
|
|
|
// Draw image
|
2023-02-28 20:40:07 +00:00
|
|
|
imageX := (pageWidth - imageWidth) / 2
|
|
|
|
|
imageY := headerHeight + (pageHeight-imageHeight)/2
|
|
|
|
|
g.pdf.ImageOptions(strings.Join(boardPath, "/"), imageX, imageY, imageWidth, imageHeight, false, opt, 0, "")
|
2023-02-15 01:28:42 +00:00
|
|
|
|
2023-03-03 06:07:41 +00:00
|
|
|
// Draw links
|
2023-02-28 04:02:33 +00:00
|
|
|
for _, shape := range shapes {
|
2023-03-03 06:07:41 +00:00
|
|
|
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
|
2023-02-28 04:02:33 +00:00
|
|
|
g.pdf.LinkString(linkX, linkY, linkWidth, linkHeight, shape.Link)
|
2023-03-03 06:07:41 +00:00
|
|
|
} 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)
|
|
|
|
|
}
|
2023-02-28 04:02:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-15 01:28:42 +00:00
|
|
|
// Draw header/img seperator
|
|
|
|
|
g.pdf.SetXY(headerMargin, headerHeight)
|
2023-02-28 20:40:07 +00:00
|
|
|
g.pdf.SetLineWidth(1)
|
2023-02-27 22:10:59 +00:00
|
|
|
if fillRGB.IsLight() {
|
|
|
|
|
g.pdf.SetDrawColor(10, 15, 37) // steel-900
|
|
|
|
|
} else {
|
|
|
|
|
g.pdf.SetDrawColor(255, 255, 255)
|
|
|
|
|
}
|
2023-02-28 20:40:07 +00:00
|
|
|
g.pdf.CellFormat(pageWidth-(headerMargin*2), 1, "", "T", 0, "", false, 0, "")
|
2023-02-15 01:28:42 +00:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *GoFPDF) Export(outputPath string) error {
|
|
|
|
|
return g.pdf.OutputFileAndClose(outputPath)
|
|
|
|
|
}
|
2023-03-03 06:07:41 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|