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…
+
+
![SVG rendered as PNG]()
+
+
+
+
+
+
+
Rendering JPEG…
+
+
![SVG rendered as 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);