diff --git a/markdown-svg-renderer.html b/markdown-svg-renderer.html index f0fe4e32..eff49cd3 100644 --- a/markdown-svg-renderer.html +++ b/markdown-svg-renderer.html @@ -310,6 +310,11 @@ return ratio; } +function svgAspectRatioNumbers(svg) { + const [w, h] = svgAspectRatio(svg).split("/").map((part) => parseFloat(part)); + return w > 0 && h > 0 ? [w, h] : [1, 1]; +} + function svgPreviewDocument(svg) { return ` @@ -357,7 +362,7 @@ border-bottom: 1px solid #e2e2e2; background: #f6f6f6; } - button { + .tabs button { font-family: inherit; font-size: 13px; border: none; @@ -368,7 +373,7 @@ border-bottom: 2px solid transparent; margin-bottom: -1px; } - button.active { + .tabs button.active { color: #1a1a1a; font-weight: bold; border-bottom-color: #2563eb; @@ -381,6 +386,41 @@ background: #fff; padding: 12px; } + .image-wrap { + width: 100%; + background: #fff; + padding: 12px; + } + .image-wrap img { + display: block; + width: 100%; + height: auto; + } + .image-actions { + padding: 0 12px 12px; + background: #fff; + } + .image-actions button { + font-family: inherit; + font-size: 13px; + border: 1px solid #e2e2e2; + border-radius: 6px; + background: #f6f6f6; + color: #1a1a1a; + padding: 6px 14px; + cursor: pointer; + } + .image-actions button:hover { + border-color: #2563eb; + color: #2563eb; + } + .image-status { + padding: 12px; + background: #fff; + font-size: 13px; + color: #666; + } + .image-status.error { color: #dc2626; } iframe { display: block; width: 100%; @@ -401,6 +441,8 @@
+ +
@@ -408,6 +450,24 @@
+
+
Rendering PNG…
+ + +
+
+
Rendering JPEG…
+ + +
`; @@ -415,7 +475,7 @@ shadow.querySelector("iframe").srcdoc = svgPreviewDocument(code); shadow.querySelector("pre").textContent = code; - const buttons = shadow.querySelectorAll("button"); + const buttons = shadow.querySelectorAll(".tabs button"); const panels = shadow.querySelectorAll(".panel"); buttons.forEach((btn) => { btn.addEventListener("click", () => { @@ -424,9 +484,85 @@ panels.forEach((p) => p.classList.toggle("active", p.dataset.panel === tab) ); + if (tab === "png" || tab === "jpeg") this.renderImage(tab); }); }); } + + // Rasterize the SVG to PNG/JPEG at the size the SVG preview is displayed, + // by drawing it onto a canvas via a blob URL (same approach as + // svg-render.html). Runs lazily on first visit to the tab. + renderImage(format) { + this._rendered = this._rendered || {}; + if (this._rendered[format]) return; + this._rendered[format] = true; + + const shadow = this.shadowRoot; + const code = this.getAttribute("data-svg") || ""; + const panel = shadow.querySelector(`.panel[data-panel="${format}"]`); + const status = panel.querySelector(".image-status"); + const imageWrap = panel.querySelector(".image-wrap"); + const imageEl = panel.querySelector("img"); + const actions = panel.querySelector(".image-actions"); + const downloadBtn = actions.querySelector("button"); + + status.hidden = false; + status.textContent = `Rendering ${format.toUpperCase()}…`; + status.classList.remove("error"); + + const fail = (message) => { + this._rendered[format] = false; + status.hidden = false; + status.textContent = message; + status.classList.add("error"); + }; + + // Match the displayed size of the SVG preview: the iframe in .svg-wrap + // is width:100% of the host content box, same as the raster . + const [ratioW, ratioH] = svgAspectRatioNumbers(code); + const width = Math.max(1, Math.round(this.clientWidth)); + const height = Math.max(1, Math.round((width * ratioH) / ratioW)); + + const mimeType = format === "png" ? "image/png" : "image/jpeg"; + const svgBlob = new Blob([code], { type: "image/svg+xml;charset=utf-8" }); + const svgUrl = URL.createObjectURL(svgBlob); + + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#fff"; + ctx.fillRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + URL.revokeObjectURL(svgUrl); + + let dataUrl; + try { + dataUrl = canvas.toDataURL(mimeType, 0.9); + } catch (err) { + fail(`Could not render ${format.toUpperCase()}: ${err.message}`); + return; + } + + status.hidden = true; + imageEl.src = dataUrl; + imageWrap.hidden = false; + actions.hidden = false; + downloadBtn.onclick = () => { + const link = document.createElement("a"); + link.href = dataUrl; + link.download = format === "png" ? "image.png" : "image.jpg"; + link.click(); + }; + }; + img.onerror = () => { + URL.revokeObjectURL(svgUrl); + fail(`Could not render this SVG as ${format.toUpperCase()}.`); + }; + img.src = svgUrl; + } } customElements.define("svg-block", SvgBlock);