Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/release/changelogs/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

#### Bugfixes ⛑️

- watch: embed local images in PNG watch previews instead of treating their URLs
as board navigation. [#2945](https://github.com/d2lang/d2/issues/2945)
- compiler:
- Limit combined map/array nesting to 128. [#2894](https://github.com/d2lang/d2/pull/2894)
- Honor cancellation during parsing and IR compilation, and prevent `d2 fmt`
Expand Down
5 changes: 3 additions & 2 deletions d2cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,15 +1019,16 @@ func _renderWithPNGEncoder(ctx context.Context, ms *xmain.State, opts d2svg.Rend
cacheImages := ms.Env.Getenv("IMG_CACHE") == "1"
if opts.MasterID == "" {
written, err := writePNGWithStatus(ctx, ms, outputPath, func(output io.Writer) error {
return renderPNGToWriter(ctx, inputPath, cacheImages, diagram, *renderOpts, pngEncoder, output)
svg, err = renderPNGToWriter(ctx, inputPath, cacheImages, diagram, *renderOpts, pngEncoder, output, svg)
return err
})
if err != nil {
return svg, written, err
}
return svg, written, nil
}
// Animated SVG composition only needs validation, not retained PNG bytes.
err = renderPNGToWriter(ctx, inputPath, cacheImages, diagram, *renderOpts, pngEncoder, io.Discard)
svg, err = renderPNGToWriter(ctx, inputPath, cacheImages, diagram, *renderOpts, pngEncoder, io.Discard, svg)
return svg, false, err
}

Expand Down
19 changes: 15 additions & 4 deletions d2cli/raster_png_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,28 @@ func pngFrameOptions() d2raster.FrameOptions {
return options
}

func renderPNGToWriter(ctx context.Context, inputPath string, cacheImages bool, diagram *d2target.Diagram, opts d2svg.RenderOpts, encoder *rasterPNGEncoder, output io.Writer) error {
document, err := buildScene(ctx, inputPath, cacheImages, diagram, opts)
func renderPNGToWriter(ctx context.Context, inputPath string, cacheImages bool, diagram *d2target.Diagram, opts d2svg.RenderOpts, encoder *rasterPNGEncoder, output io.Writer, preview []byte) ([]byte, error) {
assetOptions, err := sceneAssetOptions(ctx, inputPath, cacheImages)
if err != nil {
return err
return preview, err
}
defer assetOptions.Resolver.CloseIdleConnections()
document, err := buildSceneWithAssets(ctx, diagram, opts, assetOptions)
if err != nil {
return preview, err
}
var localEncoder rasterPNGEncoder
if encoder == nil {
encoder = &localEncoder
defer localEncoder.close()
}
return encodePNGBands(ctx, output, document, &encoder.bands)
if err := encodePNGBands(ctx, output, document, &encoder.bands); err != nil {
return preview, err
}
if len(preview) != 0 {
return bundleRasterPreview(ctx, assetOptions.Resolver, preview)
}
return preview, nil
}

func encodePNGBands(ctx context.Context, output io.Writer, document *d2scene.Document, encoder *rasterPNGBandEncoder) error {
Expand Down
4 changes: 2 additions & 2 deletions d2cli/raster_png_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestPNGStreamMatchesCompleteFrame(t *testing.T) {
t.Fatal(err)
}
var got bytes.Buffer
if err := renderPNGToWriter(context.Background(), "-", false, diagram, opts, &encoder, &got); err != nil {
if _, err := renderPNGToWriter(context.Background(), "-", false, diagram, opts, &encoder, &got, nil); err != nil {
t.Fatalf("shadow=%v scale=%v: %v", shadow, scale, err)
}
// Identical encoded bytes also verify PNG filtering across strip
Expand Down Expand Up @@ -90,7 +90,7 @@ func TestPNGStreamDiagramFixtures(t *testing.T) {
t.Fatal(err)
}
var got bytes.Buffer
if err := renderPNGToWriter(context.Background(), "-", false, &diagram, opts, nil, &got); err != nil {
if _, err := renderPNGToWriter(context.Background(), "-", false, &diagram, opts, nil, &got, nil); err != nil {
t.Fatal(err)
}
assertPNGOutputPixels(t, got.Bytes(), want)
Expand Down
39 changes: 39 additions & 0 deletions d2cli/raster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,45 @@ func TestFolderOnlyPNGReturnsFirstBoardPreview(t *testing.T) {
}
}

func TestPNGWatchPreviewEmbedsLocalImages(t *testing.T) {
directory := t.TempDir()
inputPath := filepath.Join(directory, "input.d2")
asset := testSolidPNG(t, color.NRGBA{R: 0x12, G: 0x34, B: 0x56, A: 0xff})
if err := os.WriteFile(filepath.Join(directory, "asset.png"), asset, 0o644); err != nil {
t.Fatal(err)
}
assetURL, err := url.Parse("asset.png")
if err != nil {
t.Fatal(err)
}
env := xos.NewEnv(nil)
state := &xmain.State{Env: env, Log: cmdlog.NewTB(env, t), PWD: directory}
outputPath := filepath.Join(directory, "output.png")
boards, written, err := render(
context.Background(), state, 0,
d2svg.RenderOpts{Pad: go2.Pointer(int64(0)), Scale: go2.Pointer(1.0)},
inputPath, outputPath, false, false, nil,
rasterImageDiagram(assetURL), PNG, "", true,
)
if err != nil {
t.Fatal(err)
}
if !written || len(boards) != 1 {
t.Fatalf("PNG written/preview count = %t/%d", written, len(boards))
}
want := "data:image/png;base64," + base64.StdEncoding.EncodeToString(asset)
if !bytes.Contains(boards[0], []byte(want)) || bytes.Contains(boards[0], []byte(`href="asset.png"`)) {
t.Fatal("PNG watch preview did not embed its local image")
}
output, err := os.ReadFile(outputPath)
if err != nil {
t.Fatal(err)
}
if _, err := png.Decode(bytes.NewReader(output)); err != nil {
t.Fatalf("PNG export: %v", err)
}
}

func TestBundleGIFPreviewEmbedsLocalImages(t *testing.T) {
directory := t.TempDir()
inputPath := filepath.Join(directory, "input.d2")
Expand Down