// Turns what a canvas shows into a picture or a document: the live canvas is copied (cards, notes, arrows exactly as // they look), laid out at its own size with the app's styles, fonts and images inlined, so it stands on its own. // html: a full page for the main process to print to PNG or PDF; svg: a vector file (the page inside a foreignObject) import { bounds } from './canvas.js'; const PAD = 48; // canvas units of space around the items const MAX_SIDE = 8000; // longest side in pixels, so a huge canvas still renders const XHTML = 'http://www.w3.org/1999/xhtml'; const OWN = new Set(['--gs', '--gx', '--gy', '--iz']); function dataUrl(url) { return fetch(url) .then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.blob(); }) .then((b) => new Promise((resolve, reject) => { const f = new FileReader(); f.onload = () => resolve(f.result); f.onerror = reject; f.readAsDataURL(b); })); } // A stylesheet rule's text with the files it names (fonts) written into it. async function inlineUrls(text, base) { const found = [...text.matchAll(/url\((["']?)(?!data:)([^"')]+)\1\)/g)]; for (const m of found) { try { text = text.replace(m[0], `url("${await dataUrl(new URL(m[2], base).href)}")`); } catch { /* left as it was */ } } return text; } async function pageCss() { let css = ''; const names = new Set(); const noteNames = (rule) => { if (rule.style) for (const n of rule.style) if (n.startsWith('--')) names.add(n); if (rule.cssRules) for (const r of rule.cssRules) noteNames(r); }; for (const sheet of document.styleSheets) { let rules; try { rules = sheet.cssRules; } catch { continue; } const base = sheet.href || document.baseURI; for (const r of rules) { noteNames(r); css += (r instanceof CSSFontFaceRule ? await inlineUrls(r.cssText, base) : r.cssText) + '\n'; } } return { css, names }; } const clear = (c) => !c || c === 'transparent' || /rgba?\([^)]*,\s*0\)$/.test(c); function backdrop(el) { for (let e = el; e; e = e.parentElement) { const c = getComputedStyle(e).backgroundColor; if (!clear(c)) return c; } return '#ffffff'; } // nodes: the canvas items to show (all of them); scale: pixels per canvas unit (cut down for very large canvases) export async function buildCanvasExport(view, nodes, { scale = 1 } = {}) { const b = bounds(nodes); if (!b) return null; const s = Math.max(0.05, Math.min(scale, MAX_SIDE / Math.max(b.width + PAD * 2, b.height + PAD * 2))); const W = Math.ceil((b.width + PAD * 2) * s), H = Math.ceil((b.height + PAD * 2) * s); const world = view.querySelector('.cv-world').cloneNode(true); world.style.transform = `translate(${(PAD - b.x) * s}px,${(PAD - b.y) * s}px) scale(${s})`; for (const e of world.querySelectorAll('.cv-dot,.cv-resize,.cv-box,.cv-guide')) e.remove(); for (const e of world.querySelectorAll('.sel,.target,.editing,.found,.found-cur')) e.classList.remove('sel', 'target', 'editing', 'found', 'found-cur'); await Promise.all([...world.querySelectorAll('img')].map(async (img) => { const src = img.getAttribute('src'); if (!src || src.startsWith('data:')) return; try { img.setAttribute('src', await dataUrl(src)); } catch { /* shows as the broken image it was */ } })); const { css, names } = await pageCss(); const live = getComputedStyle(view); const vars = [...names].filter((n) => !OWN.has(n)).map((n) => [n, live.getPropertyValue(n).trim()]).filter(([, v]) => v).map(([n, v]) => `${n}:${v}`); const bg = backdrop(view); const wrap = document.createElement('div'); wrap.className = 'cv'; wrap.setAttribute('style', `${vars.join(';')};--iz:${1 / s};position:relative;width:${W}px;height:${H}px;flex:none;overflow:hidden;background:${bg};` + `font-family:${live.fontFamily};font-size:${live.fontSize};color:${live.color}`); wrap.append(world); const page = `` + `` + `${wrap.outerHTML}`; // The same, as one SVG file: the styles and the copied canvas sit in a foreignObject. const style = document.createElement('style'); style.textContent = css; const inner = document.createElementNS(XHTML, 'div'); inner.append(style, wrap.cloneNode(true)); const fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); fo.setAttribute('width', String(W)); fo.setAttribute('height', String(H)); fo.append(inner); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg.setAttribute('width', String(W)); svg.setAttribute('height', String(H)); svg.setAttribute('viewBox', `0 0 ${W} ${H}`); svg.append(fo); return { html: page, svg: new XMLSerializer().serializeToString(svg), width: W, height: H, scale: s }; }