2022-11-26 03:26:14PM

This commit is contained in:
Alexander Wang 2022-11-26 15:26:14 -08:00
parent b54e376ff4
commit d328b23fb6
No known key found for this signature in database
GPG key ID: D89FA31966BDBECE
7 changed files with 1026 additions and 0 deletions

View file

@ -15,3 +15,4 @@
[#159](https://github.com/terrastruct/d2/issues/159)
- Fixes markdown newlines created with a trailing double space or backslash.
[#214](https://github.com/terrastruct/d2/pull/214)
- Fixes images not loading in PNG exports

View file

@ -18,6 +18,7 @@ import (
"oss.terrastruct.com/d2/d2renderers/textmeasure"
"oss.terrastruct.com/d2/d2themes"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
"oss.terrastruct.com/d2/lib/imgbundler"
"oss.terrastruct.com/d2/lib/png"
"oss.terrastruct.com/d2/lib/version"
"oss.terrastruct.com/d2/lib/xmain"
@ -205,6 +206,12 @@ func compile(ctx context.Context, ms *xmain.State, plugin d2plugin.Plugin, theme
return nil, err
}
// TODO this may be desirable even for SVGs. Should make it a flag
svg, err = imgbundler.Inline(ms, svg)
if err != nil {
return nil, err
}
out := svg
if filepath.Ext(outputPath) == ".png" {
out, err = png.ConvertSVG(ms, page, svg)

View file

@ -0,0 +1,95 @@
package imgbundler
import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"sync"
"time"
"oss.terrastruct.com/d2/lib/xmain"
)
var imgRe = regexp.MustCompile(`<image href="([^"]+)"`)
type resp struct {
srctxt string
data string
err error
}
func Inline(ms *xmain.State, in []byte) ([]byte, error) {
svg := string(in)
imgs := imgRe.FindAllStringSubmatch(svg, -1)
var wg sync.WaitGroup
respChan := make(chan resp)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wg.Add(len(imgs))
for _, img := range imgs {
go fetch(ctx, img[0], img[1], respChan)
}
go func() {
for {
select {
case resp, ok := <-respChan:
if !ok {
return
}
if resp.err != nil {
ms.Log.Error.Printf("image failed to fetch: %s", resp.err.Error())
} else {
svg = strings.Replace(svg, resp.srctxt, fmt.Sprintf(`<image href="%s"`, resp.data), 1)
}
wg.Done()
}
}
}()
wg.Wait()
close(respChan)
return []byte(svg), nil
}
var transport = http.DefaultTransport
func fetch(ctx context.Context, srctxt, href string, respChan chan resp) {
req, err := http.NewRequestWithContext(ctx, "GET", href, nil)
if err != nil {
respChan <- resp{err: err}
return
}
client := &http.Client{Transport: transport}
imgResp, err := client.Do(req)
if err != nil {
respChan <- resp{err: err}
return
}
defer imgResp.Body.Close()
data, err := ioutil.ReadAll(imgResp.Body)
if err != nil {
respChan <- resp{err: err}
return
}
mimeType := http.DetectContentType(data)
mimeType = strings.Replace(mimeType, "text/xml", "image/svg+xml", 1)
enc := base64.StdEncoding.EncodeToString(data)
respChan <- resp{
srctxt: srctxt,
data: fmt.Sprintf("data:%s;base64,%s", mimeType, enc),
}
}

View file

@ -0,0 +1,102 @@
package imgbundler
import (
"bytes"
_ "embed"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"oss.terrastruct.com/cmdlog"
"oss.terrastruct.com/d2/lib/xmain"
"oss.terrastruct.com/xos"
)
//go:embed test_png.png
var testPNGFile []byte
type RoundTripFunc func(req *http.Request) *http.Response
// RoundTrip .
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func TestInliner(t *testing.T) {
svgURL := "https://icons.terrastruct.com/essentials/004-picture.svg"
pngURL := "https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png"
sampleSVG := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
<svg
style="background: white;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="328" height="587" viewBox="-100 -131 328 587"><style type="text/css">
<![CDATA[
.shape {
shape-rendering: geometricPrecision;
stroke-linejoin: round;
}
.connection {
stroke-linecap: round;
stroke-linejoin: round;
}
]]>
</style><g id="a"><g class="shape" ><image href="%s" x="0" y="0" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="-15.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">a</text></g><g id="b"><g class="shape" ><image href="%s" x="0" y="228" width="128" height="128" style="fill:#FFFFFF;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" /></g><text class="text-bold" x="64.000000" y="213.000000" style="text-anchor:middle;font-size:16px;fill:#0A0F25">b</text></g><g id="(a -&gt; b)[0]"><marker id="mk-3990223579" markerWidth="10.000000" markerHeight="12.000000" refX="7.000000" refY="6.000000" viewBox="0.000000 0.000000 10.000000 12.000000" orient="auto" markerUnits="userSpaceOnUse"> <polygon class="connection" fill="#0D32B2" stroke-width="2" points="0.000000,0.000000 10.000000,6.000000 0.000000,12.000000" /> </marker><path d="M 64.000000 130.000000 C 64.000000 168.000000 64.000000 188.000000 64.000000 224.000000" class="connection" style="fill:none;stroke:#0D32B2;opacity:1.000000;stroke-width:2;" marker-end="url(#mk-3990223579)" /></g><style type="text/css"><![CDATA[
.text-bold {
font-family: "font-bold";
}
@font-face {
font-family: font-bold;
src: url("REMOVED");
}]]></style></svg>
`, svgURL, pngURL)
transport = RoundTripFunc(func(req *http.Request) *http.Response {
if req.URL.String() != svgURL && req.URL.String() != pngURL {
t.Fatal(req.URL.String())
}
var body string
switch req.URL.String() {
case svgURL:
body = `<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>\r\n<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->\r\n<svg version=\"1.1\" id=\"Capa_1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t viewBox=\"0 0 58 58\" style=\"enable-background:new 0 0 58 58;\" xml:space=\"preserve\">\r\n<rect x=\"1\" y=\"7\" style=\"fill:#C3E1ED;stroke:#E7ECED;stroke-width:2;stroke-miterlimit:10;\" width=\"56\" height=\"44\"/>\r\n<circle style=\"fill:#ED8A19;\" cx=\"16\" cy=\"17.569\" r=\"6.569\"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"56,36.111 55,35 43,24 32.5,35.5 37.983,40.983 42,45 56,45 \"/>\r\n<polygon style=\"fill:#1A9172;\" points=\"2,49 26,49 21.983,44.983 11.017,34.017 2,41.956 \"/>\r\n<rect x=\"2\" y=\"45\" style=\"fill:#6B5B4B;\" width=\"54\" height=\"5\"/>\r\n<polygon style=\"fill:#25AE88;\" points=\"37.983,40.983 27.017,30.017 10,45 42,45 \"/>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n<g>\r\n</g>\r\n</svg>`
case pngURL:
body = string(testPNGFile)
default:
t.Fatal(req.URL.String())
}
return &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
ContentLength: int64(len(body)),
Header: make(http.Header),
}
})
ms := &xmain.State{
Name: "test",
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Env: xos.NewEnv(os.Environ()),
}
ms.Log = cmdlog.Log(ms.Env, os.Stderr)
out, err := Inline(ms, []byte(sampleSVG))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(out), "https://") {
t.Fatal("links still exist")
}
if !strings.Contains(string(out), "image/svg+xml") {
t.Fatal("no svg image inserted")
}
if !strings.Contains(string(out), "image/png") {
t.Fatal("no png image inserted")
}
}

BIN
lib/imgbundler/test_png.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

821
out.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.1 MiB