From 2c4d7fc5a4456371808ab56eb90203a84d7d1c89 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Sat, 6 Jun 2026 20:18:00 +0600 Subject: [PATCH] Fix remote SVG bundling and non-interactive image rendering tests --- d2cli/main.go | 10 +- .../TestCLI_E2E/chain_icon_import.exp.svg | 2 +- go.mod | 2 +- lib/imgbundler/imgbundler.go | 55 +++++++++++ lib/imgbundler/imgbundler_test.go | 87 ++++++++++++++++++ lib/png/png.go | 5 + lib/xgif/test_output.gif | Bin 211967 -> 211967 bytes lib/xgif/xgif.go | 15 ++- lib/xgif/xgif_test.go | 13 +++ 9 files changed, 184 insertions(+), 5 deletions(-) diff --git a/d2cli/main.go b/d2cli/main.go index 6e49ad0615..2f375593a2 100644 --- a/d2cli/main.go +++ b/d2cli/main.go @@ -1511,13 +1511,21 @@ func ConvertSVG(ms *xmain.State, browser playwright.Browser, svg []byte, animInt }, time.Second*5) defer cancel() - if animIntervalMs > 0 { + if animIntervalMs > 0 && svgLooksAnimated(svg) { return xgif.ConvertAnimatedSVGToPNGs(browser, svg, animIntervalMs) } out, err := png.ConvertSVG(browser, svg) return [][]byte{out}, err } +func svgLooksAnimated(svg []byte) bool { + s := string(svg) + return strings.Contains(s, "hello + .d2-574527389 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker-d2-574527389);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker-d2-574527389);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark-d2-574527389);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker-d2-574527389);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark-d2-574527389);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal-d2-574527389);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal-d2-574527389);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright-d2-574527389);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>hello diff --git a/go.mod b/go.mod index 27317b0d08..efe87efd60 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( golang.org/x/image v0.20.0 golang.org/x/net v0.35.0 golang.org/x/sync v0.11.0 + golang.org/x/term v0.29.0 golang.org/x/text v0.22.0 golang.org/x/tools v0.25.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da @@ -51,7 +52,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/term v0.29.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lib/imgbundler/imgbundler.go b/lib/imgbundler/imgbundler.go index 7cc9c47bab..e21ce17822 100644 --- a/lib/imgbundler/imgbundler.go +++ b/lib/imgbundler/imgbundler.go @@ -2,6 +2,9 @@ package imgbundler import ( "bytes" + "compress/flate" + "compress/gzip" + "compress/zlib" "context" "encoding/base64" "fmt" @@ -18,6 +21,7 @@ import ( "sync" "time" + "github.com/andybalholm/brotli" "golang.org/x/xerrors" "oss.terrastruct.com/d2/lib/simplelog" @@ -235,11 +239,62 @@ func httpGet(ctx context.Context, l simplelog.Logger, href string) ([]byte, stri return nil, "", err } contentType := resp.Header.Get("Content-Type") + contentEncoding := resp.Header.Get("Content-Encoding") + if contentEncoding != "" { + buf, err = decodeContentEncoding(buf, contentEncoding) + if err != nil { + return nil, "", fmt.Errorf("failed to decode %q response for %s: %w", contentEncoding, href, err) + } + } l.Debug(fmt.Sprintf("fetched content type: %s, Content length: %d bytes", contentType, len(buf))) return buf, contentType, nil } +func decodeContentEncoding(buf []byte, contentEncoding string) ([]byte, error) { + encodings := strings.Split(contentEncoding, ",") + for i := len(encodings) - 1; i >= 0; i-- { + encoding := strings.TrimSpace(strings.ToLower(encodings[i])) + if encoding == "" || encoding == "identity" { + continue + } + var err error + switch encoding { + case "gzip", "x-gzip": + buf, err = gunzip(buf) + case "br": + buf, err = io.ReadAll(brotli.NewReader(bytes.NewReader(buf))) + case "deflate": + buf, err = inflate(buf) + default: + return nil, fmt.Errorf("unsupported content encoding %q", encoding) + } + if err != nil { + return nil, err + } + } + return buf, nil +} + +func gunzip(buf []byte) ([]byte, error) { + r, err := gzip.NewReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + defer r.Close() + return io.ReadAll(r) +} + +func inflate(buf []byte) ([]byte, error) { + if zr, err := zlib.NewReader(bytes.NewReader(buf)); err == nil { + defer zr.Close() + return io.ReadAll(zr) + } + fr := flate.NewReader(bytes.NewReader(buf)) + defer fr.Close() + return io.ReadAll(fr) +} + // sniffMimeType sniffs the mime type of href based on its file extension and contents. func sniffMimeType(href, buf []byte, isRemote bool) string { p := string(href) diff --git a/lib/imgbundler/imgbundler_test.go b/lib/imgbundler/imgbundler_test.go index 703c3f9972..707b3b18b0 100644 --- a/lib/imgbundler/imgbundler_test.go +++ b/lib/imgbundler/imgbundler_test.go @@ -1,9 +1,12 @@ package imgbundler import ( + "bytes" + "compress/gzip" "context" "crypto/rand" _ "embed" + "encoding/base64" "fmt" "net/http" "net/http/httptest" @@ -12,6 +15,7 @@ import ( "sync" "testing" + "github.com/andybalholm/brotli" tassert "github.com/stretchr/testify/assert" "oss.terrastruct.com/d2/lib/log" @@ -308,6 +312,89 @@ width="328" height="587" viewBox="-100 -131 328 587">