Merge pull request #1168 from ejulio-ts/gh-821-ppt-links
GH 821: links on pptx
This commit is contained in:
commit
9b3b43eb50
8 changed files with 236 additions and 70 deletions
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
|
|
||||||
"oss.terrastruct.com/d2/d2lib"
|
"oss.terrastruct.com/d2/d2lib"
|
||||||
|
"oss.terrastruct.com/d2/d2parser"
|
||||||
"oss.terrastruct.com/d2/d2plugin"
|
"oss.terrastruct.com/d2/d2plugin"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2animate"
|
"oss.terrastruct.com/d2/d2renderers/d2animate"
|
||||||
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
||||||
|
|
@ -32,7 +33,6 @@ import (
|
||||||
"oss.terrastruct.com/d2/lib/background"
|
"oss.terrastruct.com/d2/lib/background"
|
||||||
"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/pptx"
|
"oss.terrastruct.com/d2/lib/pptx"
|
||||||
|
|
@ -190,7 +190,7 @@ func Run(ctx context.Context, ms *xmain.State) (err error) {
|
||||||
outputPath = ms.AbsPath(outputPath)
|
outputPath = ms.AbsPath(outputPath)
|
||||||
if *animateIntervalFlag > 0 {
|
if *animateIntervalFlag > 0 {
|
||||||
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
|
// Not checking for extension == "svg", because users may want to write SVG data to a non-svg-extension file
|
||||||
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" {
|
if filepath.Ext(outputPath) == ".png" || filepath.Ext(outputPath) == ".pdf" || filepath.Ext(outputPath) == ".pptx" {
|
||||||
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
|
return xmain.UsageErrorf("-animate-interval can only be used when exporting to SVG.\nYou provided: %s", filepath.Ext(outputPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +352,7 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
||||||
|
|
||||||
switch filepath.Ext(outputPath) {
|
switch filepath.Ext(outputPath) {
|
||||||
case ".pdf":
|
case ".pdf":
|
||||||
pageMap := pdf.BuildPDFPageMap(diagram, nil, nil)
|
pageMap := buildBoardIDToIndex(diagram, nil, nil)
|
||||||
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pdf, false, err
|
return pdf, false, err
|
||||||
|
|
@ -369,7 +369,9 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
||||||
rootName := getFileName(outputPath)
|
rootName := getFileName(outputPath)
|
||||||
// version must be only numbers to avoid issues with PowerPoint
|
// version must be only numbers to avoid issues with PowerPoint
|
||||||
p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers())
|
p := pptx.NewPresentation(rootName, description, rootName, username, version.OnlyNumbers())
|
||||||
svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, outputPath, page, diagram, nil)
|
|
||||||
|
boardIdToIndex := buildBoardIDToIndex(diagram, nil, nil)
|
||||||
|
svg, err := renderPPTX(ctx, ms, p, plugin, renderOpts, ruler, outputPath, page, diagram, nil, boardIdToIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
@ -758,7 +760,7 @@ func renderPDF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opt
|
||||||
return svg, nil
|
return svg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string) ([]byte, error) {
|
func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Presentation, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, outputPath string, page playwright.Page, diagram *d2target.Diagram, boardPath []string, boardIdToIndex map[string]int) ([]byte, error) {
|
||||||
var currBoardPath []string
|
var currBoardPath []string
|
||||||
// Root board doesn't have a name, so we use the output filename
|
// Root board doesn't have a name, so we use the output filename
|
||||||
if diagram.Name == "" {
|
if diagram.Name == "" {
|
||||||
|
|
@ -796,31 +798,71 @@ func renderPPTX(ctx context.Context, ms *xmain.State, presentation *pptx.Present
|
||||||
return nil, bundleErr
|
return nil, bundleErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svg = appendix.Append(diagram, ruler, svg)
|
||||||
|
|
||||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
pngImg, err := png.ConvertSVG(ms, page, svg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = presentation.AddSlide(pngImg, currBoardPath)
|
slide, err := presentation.AddSlide(pngImg, currBoardPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewboxSlice := appendix.FindViewboxSlice(svg)
|
||||||
|
viewboxX, err := strconv.ParseFloat(viewboxSlice[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
viewboxY, err := strconv.ParseFloat(viewboxSlice[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw links
|
||||||
|
for _, shape := range diagram.Shapes {
|
||||||
|
if shape.Link == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
linkX := png.SCALE * (float64(shape.Pos.X) - viewboxX - float64(shape.StrokeWidth))
|
||||||
|
linkY := png.SCALE * (float64(shape.Pos.Y) - viewboxY - float64(shape.StrokeWidth))
|
||||||
|
linkWidth := png.SCALE * (float64(shape.Width) + float64(shape.StrokeWidth*2))
|
||||||
|
linkHeight := png.SCALE * (float64(shape.Height) + float64(shape.StrokeWidth*2))
|
||||||
|
link := &pptx.Link{
|
||||||
|
Left: int(linkX),
|
||||||
|
Top: int(linkY),
|
||||||
|
Width: int(linkWidth),
|
||||||
|
Height: int(linkHeight),
|
||||||
|
Tooltip: shape.Link,
|
||||||
|
}
|
||||||
|
slide.AddLink(link)
|
||||||
|
key, err := d2parser.ParseKey(shape.Link)
|
||||||
|
if err != nil || key.Path[0].Unbox().ScalarString() != "root" {
|
||||||
|
// External link
|
||||||
|
link.ExternalUrl = shape.Link
|
||||||
|
} else if pageNum, ok := boardIdToIndex[shape.Link]; ok {
|
||||||
|
// Internal link
|
||||||
|
link.SlideIndex = pageNum + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dl := range diagram.Layers {
|
for _, dl := range diagram.Layers {
|
||||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Scenarios {
|
for _, dl := range diagram.Scenarios {
|
||||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dl := range diagram.Steps {
|
for _, dl := range diagram.Steps {
|
||||||
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, "", page, dl, currBoardPath)
|
_, err := renderPPTX(ctx, ms, presentation, plugin, opts, ruler, "", page, dl, currBoardPath, boardIdToIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -922,3 +964,28 @@ func loadFonts(ms *xmain.State, pathToRegular, pathToItalic, pathToBold, pathToS
|
||||||
|
|
||||||
return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
|
return d2fonts.AddFontFamily("custom", regularTTF, italicTTF, boldTTF, semiboldTTF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildBoardIDToIndex returns a map from board path to page int
|
||||||
|
// To map correctly, it must follow the same traversal of pdf/pptx building
|
||||||
|
func buildBoardIDToIndex(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 {
|
||||||
|
buildBoardIDToIndex(dl, dictionary, append(newPath, "layers"))
|
||||||
|
}
|
||||||
|
for _, dl := range diagram.Scenarios {
|
||||||
|
buildBoardIDToIndex(dl, dictionary, append(newPath, "scenarios"))
|
||||||
|
}
|
||||||
|
for _, dl := range diagram.Steps {
|
||||||
|
buildBoardIDToIndex(dl, dictionary, append(newPath, "steps"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,9 @@ layers: {
|
||||||
name: "how_to_solve_problems_pptx",
|
name: "how_to_solve_problems_pptx",
|
||||||
skipCI: true,
|
skipCI: true,
|
||||||
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
run: func(t *testing.T, ctx context.Context, dir string, env *xos.Env) {
|
||||||
writeFile(t, dir, "in.d2", `how to solve a hard problem?
|
writeFile(t, dir, "in.d2", `how to solve a hard problem? {
|
||||||
|
link: steps.2
|
||||||
|
}
|
||||||
steps: {
|
steps: {
|
||||||
1: {
|
1: {
|
||||||
w: write down the problem
|
w: write down the problem
|
||||||
|
|
@ -261,6 +263,9 @@ steps: {
|
||||||
3: {
|
3: {
|
||||||
t -> w2
|
t -> w2
|
||||||
w2: write down the solution
|
w2: write down the solution
|
||||||
|
w2: {
|
||||||
|
link: https://d2lang.com
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
|
||||||
|
|
@ -164,28 +164,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
async (imgString) => {
|
async ({imgString, scale}) => {
|
||||||
const tempImg = new Image();
|
const tempImg = new Image();
|
||||||
const loadImage = () => {
|
const loadImage = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
tempImg.onload = (event) => resolve(event.currentTarget);
|
tempImg.onload = (event) => resolve(event.currentTarget);
|
||||||
tempImg.onerror = () => {
|
tempImg.onerror = () => {
|
||||||
reject("error loading string as an image");
|
reject("error loading string as an image:\n" + imgString);
|
||||||
};
|
};
|
||||||
tempImg.src = imgString;
|
tempImg.src = imgString;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const img = await loadImage();
|
const img = await loadImage();
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = img.width * 2;
|
canvas.width = img.width * scale;
|
||||||
canvas.height = img.height * 2;
|
canvas.height = img.height * scale;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return new Error("could not get canvas context");
|
return new Error("could not get canvas context");
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ import (
|
||||||
"oss.terrastruct.com/util-go/xmain"
|
"oss.terrastruct.com/util-go/xmain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ConvertSVG scales the image by 2x
|
||||||
|
const SCALE = 2.
|
||||||
|
|
||||||
type Playwright struct {
|
type Playwright struct {
|
||||||
PW *playwright.Playwright
|
PW *playwright.Playwright
|
||||||
Browser playwright.Browser
|
Browser playwright.Browser
|
||||||
|
|
@ -83,6 +86,8 @@ var genPNGScript string
|
||||||
|
|
||||||
const pngPrefix = "data:image/png;base64,"
|
const pngPrefix = "data:image/png;base64,"
|
||||||
|
|
||||||
|
// ConvertSVG converts the given SVG into a PNG.
|
||||||
|
// Note that the resulting PNG has 2x the size (width and height) of the original SVG (see generate_png.js)
|
||||||
func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
|
func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, error) {
|
||||||
cancel := background.Repeat(func() {
|
cancel := background.Repeat(func() {
|
||||||
ms.Log.Info.Printf("converting to PNG...")
|
ms.Log.Info.Printf("converting to PNG...")
|
||||||
|
|
@ -90,7 +95,10 @@ func ConvertSVG(ms *xmain.State, page playwright.Page, svg []byte) ([]byte, erro
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
encodedSVG := base64.StdEncoding.EncodeToString(svg)
|
encodedSVG := base64.StdEncoding.EncodeToString(svg)
|
||||||
pngInterface, err := page.Evaluate(genPNGScript, "data:image/svg+xml;charset=utf-8;base64,"+encodedSVG)
|
pngInterface, err := page.Evaluate(genPNGScript, map[string]interface{}{
|
||||||
|
"imgString": "data:image/svg+xml;charset=utf-8;base64," + encodedSVG,
|
||||||
|
"scale": int(SCALE),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate png: %w", err)
|
return nil, fmt.Errorf("failed to generate png: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
125
lib/pptx/pptx.go
125
lib/pptx/pptx.go
|
|
@ -32,14 +32,38 @@ type Presentation struct {
|
||||||
|
|
||||||
Slides []*Slide
|
Slides []*Slide
|
||||||
}
|
}
|
||||||
|
|
||||||
type Slide struct {
|
type Slide struct {
|
||||||
BoardPath []string
|
BoardPath []string
|
||||||
Image []byte
|
Links []*Link
|
||||||
ImageWidth int
|
Image []byte
|
||||||
ImageHeight int
|
ImageId string
|
||||||
ImageTop int
|
ImageWidth int
|
||||||
ImageLeft int
|
ImageHeight int
|
||||||
|
ImageTop int
|
||||||
|
ImageLeft int
|
||||||
|
ImageScaleFactor float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Slide) AddLink(link *Link) {
|
||||||
|
link.Index = len(s.Links)
|
||||||
|
s.Links = append(s.Links, link)
|
||||||
|
link.ID = fmt.Sprintf("link%d", len(s.Links))
|
||||||
|
link.Height *= int(s.ImageScaleFactor)
|
||||||
|
link.Width *= int(s.ImageScaleFactor)
|
||||||
|
link.Top = s.ImageTop + int(float64(link.Top)*s.ImageScaleFactor)
|
||||||
|
link.Left = s.ImageLeft + int(float64(link.Left)*s.ImageScaleFactor)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
ID string
|
||||||
|
Index int
|
||||||
|
Top int
|
||||||
|
Left int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
SlideIndex int
|
||||||
|
ExternalUrl string
|
||||||
|
Tooltip string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPresentation(title, description, subject, creator, d2Version string) *Presentation {
|
func NewPresentation(title, description, subject, creator, d2Version string) *Presentation {
|
||||||
|
|
@ -52,10 +76,10 @@ func NewPresentation(title, description, subject, creator, d2Version string) *Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error {
|
func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) (*Slide, error) {
|
||||||
src, err := png.Decode(bytes.NewReader(pngContent))
|
src, err := png.Decode(bytes.NewReader(pngContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error decoding PNG image: %v", err)
|
return nil, fmt.Errorf("error decoding PNG image: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var width, height int
|
var width, height int
|
||||||
|
|
@ -99,22 +123,24 @@ func (p *Presentation) AddSlide(pngContent []byte, boardPath []string) error {
|
||||||
height = IMAGE_HEIGHT
|
height = IMAGE_HEIGHT
|
||||||
width = int(float64(height) * (srcWidth / srcHeight))
|
width = int(float64(height) * (srcWidth / srcHeight))
|
||||||
}
|
}
|
||||||
top := (IMAGE_HEIGHT - height) / 2
|
top := HEADER_HEIGHT + ((IMAGE_HEIGHT - height) / 2)
|
||||||
left := (SLIDE_WIDTH - width) / 2
|
left := (SLIDE_WIDTH - width) / 2
|
||||||
|
|
||||||
slide := &Slide{
|
slide := &Slide{
|
||||||
BoardPath: make([]string, len(boardPath)),
|
BoardPath: make([]string, len(boardPath)),
|
||||||
Image: pngContent,
|
ImageId: fmt.Sprintf("slide%dImage", len(p.Slides)+1),
|
||||||
ImageWidth: width,
|
Image: pngContent,
|
||||||
ImageHeight: height,
|
ImageWidth: width,
|
||||||
ImageTop: top,
|
ImageHeight: height,
|
||||||
ImageLeft: left,
|
ImageTop: top,
|
||||||
|
ImageLeft: left,
|
||||||
|
ImageScaleFactor: float64(width) / srcWidth,
|
||||||
}
|
}
|
||||||
// it must copy the board path to avoid slice reference issues
|
// it must copy the board path to avoid slice reference issues
|
||||||
copy(slide.BoardPath, boardPath)
|
copy(slide.BoardPath, boardPath)
|
||||||
|
|
||||||
p.Slides = append(p.Slides, slide)
|
p.Slides = append(p.Slides, slide)
|
||||||
return nil
|
return slide, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Presentation) SaveTo(filePath string) error {
|
func (p *Presentation) SaveTo(filePath string) error {
|
||||||
|
|
@ -145,10 +171,7 @@ func (p *Presentation) SaveTo(filePath string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, RelsSlideXmlContent{
|
err = addFileFromTemplate(zipWriter, fmt.Sprintf("ppt/slides/_rels/%s.xml.rels", slideFileName), RELS_SLIDE_XML, getSlideXmlRelsContent(imageID, slide))
|
||||||
FileName: imageID,
|
|
||||||
RelationshipID: imageID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -242,14 +265,49 @@ func copyPptxTemplateTo(w *zip.Writer) error {
|
||||||
//go:embed templates/slide.xml.rels
|
//go:embed templates/slide.xml.rels
|
||||||
var RELS_SLIDE_XML string
|
var RELS_SLIDE_XML string
|
||||||
|
|
||||||
|
type RelsSlideXmlLinkContent struct {
|
||||||
|
RelationshipID string
|
||||||
|
ExternalUrl string
|
||||||
|
SlideIndex int
|
||||||
|
}
|
||||||
|
|
||||||
type RelsSlideXmlContent struct {
|
type RelsSlideXmlContent struct {
|
||||||
FileName string
|
FileName string
|
||||||
RelationshipID string
|
RelationshipID string
|
||||||
|
Links []RelsSlideXmlLinkContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSlideXmlRelsContent(imageID string, slide *Slide) RelsSlideXmlContent {
|
||||||
|
content := RelsSlideXmlContent{
|
||||||
|
FileName: imageID,
|
||||||
|
RelationshipID: imageID,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, link := range slide.Links {
|
||||||
|
content.Links = append(content.Links, RelsSlideXmlLinkContent{
|
||||||
|
RelationshipID: link.ID,
|
||||||
|
ExternalUrl: link.ExternalUrl,
|
||||||
|
SlideIndex: link.SlideIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed templates/slide.xml
|
//go:embed templates/slide.xml
|
||||||
var SLIDE_XML string
|
var SLIDE_XML string
|
||||||
|
|
||||||
|
type SlideLinkXmlContent struct {
|
||||||
|
ID int
|
||||||
|
RelationshipID string
|
||||||
|
Name string
|
||||||
|
Action string
|
||||||
|
Left int
|
||||||
|
Top int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
type SlideXmlContent struct {
|
type SlideXmlContent struct {
|
||||||
Title string
|
Title string
|
||||||
TitlePrefix string
|
TitlePrefix string
|
||||||
|
|
@ -260,6 +318,8 @@ type SlideXmlContent struct {
|
||||||
ImageTop int
|
ImageTop int
|
||||||
ImageWidth int
|
ImageWidth int
|
||||||
ImageHeight int
|
ImageHeight int
|
||||||
|
|
||||||
|
Links []SlideLinkXmlContent
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent {
|
func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent {
|
||||||
|
|
@ -270,17 +330,36 @@ func getSlideXmlContent(imageID string, slide *Slide) SlideXmlContent {
|
||||||
if len(prefixPath) > 0 {
|
if len(prefixPath) > 0 {
|
||||||
prefix = strings.Join(prefixPath, " / ") + " / "
|
prefix = strings.Join(prefixPath, " / ") + " / "
|
||||||
}
|
}
|
||||||
return SlideXmlContent{
|
content := SlideXmlContent{
|
||||||
Title: boardName,
|
Title: boardName,
|
||||||
TitlePrefix: prefix,
|
TitlePrefix: prefix,
|
||||||
Description: strings.Join(boardPath, " / "),
|
Description: strings.Join(boardPath, " / "),
|
||||||
HeaderHeight: HEADER_HEIGHT,
|
HeaderHeight: HEADER_HEIGHT,
|
||||||
ImageID: imageID,
|
ImageID: imageID,
|
||||||
ImageLeft: slide.ImageLeft,
|
ImageLeft: slide.ImageLeft,
|
||||||
ImageTop: slide.ImageTop + HEADER_HEIGHT,
|
ImageTop: slide.ImageTop,
|
||||||
ImageWidth: slide.ImageWidth,
|
ImageWidth: slide.ImageWidth,
|
||||||
ImageHeight: slide.ImageHeight,
|
ImageHeight: slide.ImageHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, link := range slide.Links {
|
||||||
|
var action string
|
||||||
|
if link.ExternalUrl == "" {
|
||||||
|
action = "ppaction://hlinksldjump"
|
||||||
|
}
|
||||||
|
content.Links = append(content.Links, SlideLinkXmlContent{
|
||||||
|
ID: link.Index,
|
||||||
|
RelationshipID: link.ID,
|
||||||
|
Name: link.Tooltip,
|
||||||
|
Action: action,
|
||||||
|
Left: link.Left,
|
||||||
|
Top: link.Top,
|
||||||
|
Width: link.Width,
|
||||||
|
Height: link.Height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed templates/rels_presentation.xml
|
//go:embed templates/rels_presentation.xml
|
||||||
|
|
|
||||||
|
|
@ -75,19 +75,44 @@
|
||||||
<a:defRPr sz="2400" />
|
<a:defRPr sz="2400" />
|
||||||
</a:lvl1pPr>
|
</a:lvl1pPr>
|
||||||
</a:lstStyle>
|
</a:lstStyle>
|
||||||
<a:p>
|
<a:p> {{if .TitlePrefix}} <a:r>
|
||||||
{{if .TitlePrefix}}
|
|
||||||
<a:r>
|
|
||||||
<a:t>{{.TitlePrefix}}</a:t>
|
<a:t>{{.TitlePrefix}}</a:t>
|
||||||
</a:r>
|
</a:r> {{end}} <a:r>
|
||||||
{{end}}
|
|
||||||
<a:r>
|
|
||||||
<a:rPr b="1" />
|
<a:rPr b="1" />
|
||||||
<a:t>{{.Title}}</a:t>
|
<a:t>{{.Title}}</a:t>
|
||||||
</a:r>
|
</a:r>
|
||||||
</a:p>
|
</a:p>
|
||||||
</p:txBody>
|
</p:txBody>
|
||||||
</p:sp>
|
</p:sp>
|
||||||
|
{{range .Links}}
|
||||||
|
<p:sp>
|
||||||
|
<p:nvSpPr>
|
||||||
|
<p:cNvPr id="{{.ID}}" name="{{.Name}}">
|
||||||
|
<a:hlinkClick r:id="{{.RelationshipID}}" action="{{.Action}}" tooltip="{{.Name}}" history="1" invalidUrl=""
|
||||||
|
tgtFrame="" highlightClick="0" endSnd="0" />
|
||||||
|
</p:cNvPr>
|
||||||
|
<p:cNvSpPr />
|
||||||
|
<p:nvPr />
|
||||||
|
</p:nvSpPr>
|
||||||
|
<p:spPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="{{.Left}}" y="{{.Top}}" />
|
||||||
|
<a:ext cx="{{.Width}}" cy="{{.Height}}" />
|
||||||
|
</a:xfrm>
|
||||||
|
<a:prstGeom prst="rect">
|
||||||
|
<a:avLst />
|
||||||
|
</a:prstGeom>
|
||||||
|
<a:solidFill>
|
||||||
|
<a:srgbClr val="FFFFFF">
|
||||||
|
<a:alpha val="0" />
|
||||||
|
</a:srgbClr>
|
||||||
|
</a:solidFill>
|
||||||
|
<a:ln w="12700">
|
||||||
|
<a:miter lim="400000" />
|
||||||
|
</a:ln>
|
||||||
|
</p:spPr>
|
||||||
|
</p:sp>
|
||||||
|
{{end}}
|
||||||
</p:spTree>
|
</p:spTree>
|
||||||
</p:cSld>
|
</p:cSld>
|
||||||
<p:clrMapOvr>
|
<p:clrMapOvr>
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,11 @@
|
||||||
<Relationship Id="{{.RelationshipID}}"
|
<Relationship Id="{{.RelationshipID}}"
|
||||||
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||||
Target="../media/{{.FileName}}.png" />
|
Target="../media/{{.FileName}}.png" />
|
||||||
|
{{range .Links}}
|
||||||
|
{{if .ExternalUrl}}
|
||||||
|
<Relationship Id="{{.RelationshipID}}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="{{.ExternalUrl}}" TargetMode="External" />
|
||||||
|
{{else}}
|
||||||
|
<Relationship Id="{{.RelationshipID}}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slide{{.SlideIndex}}.xml" />
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</Relationships>
|
</Relationships>
|
||||||
Loading…
Reference in a new issue