export gifs
This commit is contained in:
parent
9fc5a5f4ce
commit
8b3fea259c
4 changed files with 137 additions and 36 deletions
|
|
@ -355,6 +355,26 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
|||
|
||||
ext := getExportExtension(outputPath)
|
||||
switch ext {
|
||||
case GIF:
|
||||
svg, pngs, err := renderPNGsForGIF(ctx, ms, plugin, renderOpts, ruler, page, diagram)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
out, err := xgif.AnimatePNGs(pngs, int(animateInterval))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
err = ms.WritePath(outputPath, out)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
dur := time.Since(start)
|
||||
ms.Log.Success.Printf("successfully compiled %s to %s in %s", ms.HumanPath(inputPath), ms.HumanPath(outputPath), dur)
|
||||
return svg, true, nil
|
||||
case PDF:
|
||||
pageMap := buildBoardIDToIndex(diagram, nil, nil)
|
||||
pdf, err := renderPDF(ctx, ms, plugin, renderOpts, outputPath, page, ruler, diagram, nil, nil, pageMap)
|
||||
|
|
@ -401,19 +421,11 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, rende
|
|||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
// TODO: test GIF with 1 frame
|
||||
var out []byte
|
||||
if len(boards) > 0 {
|
||||
out = boards[0]
|
||||
if animateInterval > 0 {
|
||||
if ext == GIF {
|
||||
tl, br := diagram.NestedBoundingBox()
|
||||
width := png.SCALE*(br.X-tl.X) + renderOpts.Pad*2
|
||||
height := png.SCALE*(br.Y-tl.Y) + renderOpts.Pad*2
|
||||
out, err = xgif.AnimatePNGs(boards, width, height, int(animateInterval))
|
||||
} else {
|
||||
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
|
||||
}
|
||||
if animateInterval > 0 && ext != GIF {
|
||||
out, err = d2animate.Wrap(diagram, boards, renderOpts, int(animateInterval))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
|
@ -610,7 +622,7 @@ func render(ctx context.Context, ms *xmain.State, compileDur time.Duration, plug
|
|||
}
|
||||
|
||||
func _render(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, outputPath string, bundle, forceAppendix bool, page playwright.Page, ruler *textmeasure.Ruler, diagram *d2target.Diagram) ([]byte, error) {
|
||||
toPNG := getExportExtension(outputPath).requiresPNGRenderer()
|
||||
toPNG := getExportExtension(outputPath) == PNG
|
||||
svg, err := d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
|
|
@ -1001,3 +1013,61 @@ func buildBoardIDToIndex(diagram *d2target.Diagram, dictionary map[string]int, p
|
|||
|
||||
return dictionary
|
||||
}
|
||||
|
||||
func renderPNGsForGIF(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, opts d2svg.RenderOpts, ruler *textmeasure.Ruler, page playwright.Page, diagram *d2target.Diagram) (svg []byte, pngs [][]byte, err error) {
|
||||
if !diagram.IsFolderOnly {
|
||||
svg, err = d2svg.Render(diagram, &d2svg.RenderOpts{
|
||||
Pad: opts.Pad,
|
||||
Sketch: opts.Sketch,
|
||||
Center: opts.Center,
|
||||
SetDimensions: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
svg, err = plugin.PostProcess(ctx, svg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
svg, bundleErr := imgbundler.BundleLocal(ctx, ms, svg)
|
||||
svg, bundleErr2 := imgbundler.BundleRemote(ctx, ms, svg)
|
||||
bundleErr = multierr.Combine(bundleErr, bundleErr2)
|
||||
if bundleErr != nil {
|
||||
return nil, nil, bundleErr
|
||||
}
|
||||
|
||||
svg = appendix.Append(diagram, ruler, svg)
|
||||
|
||||
pngImg, err := png.ConvertSVG(ms, page, svg)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, pngImg)
|
||||
}
|
||||
|
||||
for _, dl := range diagram.Layers {
|
||||
_, layerPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, layerPNGs...)
|
||||
}
|
||||
for _, dl := range diagram.Scenarios {
|
||||
_, scenarioPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, scenarioPNGs...)
|
||||
}
|
||||
for _, dl := range diagram.Steps {
|
||||
_, stepsPNGs, err := renderPNGsForGIF(ctx, ms, plugin, opts, ruler, page, dl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pngs = append(pngs, stepsPNGs...)
|
||||
}
|
||||
|
||||
return svg, pngs, nil
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 219 KiB |
|
|
@ -4,32 +4,47 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
|
||||
"github.com/ericpauley/go-quantize/quantize"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
const INFINITE_LOOP = 0
|
||||
const BG_INDEX uint8 = 255
|
||||
|
||||
var BG_COLOR = color.White
|
||||
|
||||
func AnimatePNGs(pngs [][]byte, animIntervalMs int) ([]byte, error) {
|
||||
var width, height int
|
||||
pngImgs := make([]image.Image, len(pngs))
|
||||
for i, pngBytes := range pngs {
|
||||
img, err := png.Decode(bytes.NewBuffer(pngBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pngImgs[i] = img
|
||||
bounds := img.Bounds()
|
||||
width = go2.Max(width, bounds.Dx())
|
||||
height = go2.Max(height, bounds.Dy())
|
||||
}
|
||||
|
||||
func AnimatePNGs(pngs [][]byte, gifWidth, gifHeight int, animIntervalMs int) ([]byte, error) {
|
||||
interval := animIntervalMs / 10 // gif animation interval is in 100ths of a second
|
||||
anim := &gif.GIF{
|
||||
LoopCount: INFINITE_LOOP,
|
||||
Config: image.Config{
|
||||
Width: gifWidth,
|
||||
Height: gifHeight,
|
||||
Width: width,
|
||||
Height: height,
|
||||
},
|
||||
}
|
||||
|
||||
for _, pngBytes := range pngs {
|
||||
pngImage, err := png.Decode(bytes.NewBuffer(pngBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, pngImage := range pngImgs {
|
||||
// 1. convert the PNG into a GIF compatible image (Bitmap) by quantizing it to 255 colors
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = gif.Encode(buf, pngImage, &gif.Options{
|
||||
NumColors: 256, // GIFs can have up to 256 colors
|
||||
err := gif.Encode(buf, pngImage, &gif.Options{
|
||||
NumColors: 255, // GIFs can have up to 256 colors, so keep 1 slot for white background
|
||||
Quantizer: quantize.MedianCutQuantizer{},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -43,7 +58,27 @@ func AnimatePNGs(pngs [][]byte, gifWidth, gifHeight int, animIntervalMs int) ([]
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("decoded git image could not be cast as *image.Paletted")
|
||||
}
|
||||
anim.Image = append(anim.Image, palettedImg)
|
||||
|
||||
// 2. make GIF frames of the same size, keeping images centered and with a white background
|
||||
bounds := pngImage.Bounds()
|
||||
top := (height - bounds.Dy()) / 2
|
||||
bottom := top + bounds.Dy()
|
||||
left := (width - bounds.Dx()) / 2
|
||||
right := left + bounds.Dx()
|
||||
|
||||
palettedImg.Palette[BG_INDEX] = BG_COLOR
|
||||
frame := image.NewPaletted(image.Rect(0, 0, width, height), palettedImg.Palette)
|
||||
for x := 0; x < width; x++ {
|
||||
for y := 0; y < height; y++ {
|
||||
if x <= left || y <= top || x >= right || y >= bottom {
|
||||
frame.SetColorIndex(x, y, BG_INDEX)
|
||||
} else {
|
||||
frame.SetColorIndex(x, y, palettedImg.ColorIndexAt(x-left, y-top))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anim.Image = append(anim.Image, frame)
|
||||
anim.Delay = append(anim.Delay, interval)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
package xgif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"image/png"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
//go:embed test_input1.png
|
||||
|
|
@ -20,20 +18,18 @@ var test_input2 []byte
|
|||
var test_output []byte
|
||||
|
||||
func TestPngToGif(t *testing.T) {
|
||||
board1, err := png.Decode(bytes.NewBuffer(test_input1))
|
||||
assert.NoError(t, err)
|
||||
gifWidth := board1.Bounds().Dx()
|
||||
gifHeight := board1.Bounds().Dy()
|
||||
|
||||
board2, err := png.Decode(bytes.NewBuffer(test_input2))
|
||||
assert.NoError(t, err)
|
||||
gifWidth = go2.Max(board2.Bounds().Dx(), gifWidth)
|
||||
gifHeight = go2.Max(board2.Bounds().Dy(), gifHeight)
|
||||
|
||||
boards := [][]byte{test_input1, test_input2}
|
||||
interval := 1_000
|
||||
gifBytes, err := AnimatePNGs(boards, gifWidth, gifHeight, interval)
|
||||
gifBytes, err := AnimatePNGs(boards, interval)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// use this to update the test output
|
||||
if false {
|
||||
f, err := os.Create("test_output_2.gif")
|
||||
assert.NoError(t, err)
|
||||
defer f.Close()
|
||||
f.Write(gifBytes)
|
||||
}
|
||||
|
||||
assert.Equal(t, test_output, gifBytes)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue