diff --git a/addon-build/docx-editor/fetch-fonts.mjs b/addon-build/docx-editor/fetch-fonts.mjs new file mode 100644 index 0000000..9dd0414 --- /dev/null +++ b/addon-build/docx-editor/fetch-fonts.mjs @@ -0,0 +1,85 @@ +// Vendor the editor's bundled webfonts. +// +// npm run fonts (from addon-build/docx-editor/) +// +// The editor offers fonts in its ribbon, and a font the machine doesn't have +// is a font the user picks and then cannot see. Ubuntu and Fraunces are not +// on Windows, so they ship with the add-on. +// +// Fetched from Google Fonts once, here, and committed — never at runtime. An +// extension in a browser built around not phoning home should not ask +// fonts.gstatic.com what a document looks like every time one is opened. +// +// Subsets: Latin plus Cyrillic and Greek where the family has them, because +// the documents this editor is actually used on are not all English. +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const OUT = path.resolve(here, "../../bundled-addons/docx-editor/fonts"); + +// A browser UA, or Google serves ttf instead of woff2. +const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; + +const FAMILIES = [ + { + name: "Ubuntu", + css: "https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,400;0,700;1,400;1,700&display=block", + // No cyrillic-ext or greek-ext: 148 KB for historic and liturgical + // ranges that a word processor's font menu will not miss. + keep: ["latin", "latin-ext", "cyrillic", "greek"], + }, + { + // The variable file, which is smaller than the four static instances it + // replaces (280 KB against 305 KB) and carries every weight between them. + name: "Fraunces", + css: "https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=block", + keep: ["latin", "latin-ext"], + }, +]; + +async function text(url) { + const r = await fetch(url, { headers: { "User-Agent": UA } }); + if (!r.ok) throw new Error(`${r.status} for ${url}`); + return r.text(); +} + +fs.mkdirSync(OUT, { recursive: true }); +let css = `/* Bundled with the Word editor — see addon-build/docx-editor/fetch-fonts.mjs. + Regenerate with \`npm run fonts\`; do not edit by hand. */\n`; +let total = 0; + +for (const fam of FAMILIES) { + const sheet = await text(fam.css); + // Google's stylesheet is a run of /* subset */ + @font-face blocks. + const blocks = sheet.split("/*").slice(1); + let n = 0; + for (const raw of blocks) { + const subset = raw.slice(0, raw.indexOf("*/")).trim(); + if (!fam.keep.includes(subset)) continue; + const block = raw.slice(raw.indexOf("*/") + 2); + const url = /src:\s*url\(([^)]+)\)/.exec(block)?.[1]; + if (!url) continue; + const style = /font-style:\s*(\w+)/.exec(block)?.[1] || "normal"; + const weight = /font-weight:\s*([\d\s]+)/.exec(block)?.[1].trim() || "400"; + const range = /unicode-range:\s*([^;]+);/.exec(block)?.[1].trim(); + + const file = `${fam.name.toLowerCase()}-${subset}-${style}-${weight.replace(/\s+/g, "_")}.woff2`; + const bytes = Buffer.from(await (await fetch(url, { headers: { "User-Agent": UA } })).arrayBuffer()); + fs.writeFileSync(path.join(OUT, file), bytes); + total += bytes.length; + n++; + + css += `@font-face {\n font-family: '${fam.name}';\n font-style: ${style};\n` + + ` font-weight: ${weight};\n font-display: block;\n` + + ` src: url('fonts/${file}') format('woff2');\n` + + (range ? ` unicode-range: ${range};\n` : "") + `}\n`; + } + console.log(`${fam.name}: ${n} file(s)`); +} + +fs.writeFileSync(path.resolve(OUT, "../fonts.css"), css); +console.log(`\n${(total / 1024).toFixed(0)} KB of woff2 in ${path.relative(path.resolve(here, "../.."), OUT)}`); +console.log(`stylesheet: bundled-addons/docx-editor/fonts.css`); diff --git a/addon-build/docx-editor/package.json b/addon-build/docx-editor/package.json index 275b0f3..ee39050 100644 --- a/addon-build/docx-editor/package.json +++ b/addon-build/docx-editor/package.json @@ -6,7 +6,8 @@ "type": "module", "scripts": { "build": "node build.mjs", - "icons": "node make-icons.mjs" + "icons": "node make-icons.mjs", + "fonts": "node fetch-fonts.mjs" }, "devDependencies": { "docx": "^9.5.1", diff --git a/bundled-addons/docx-editor/README.md b/bundled-addons/docx-editor/README.md index c025f53..a3a5969 100644 --- a/bundled-addons/docx-editor/README.md +++ b/bundled-addons/docx-editor/README.md @@ -28,6 +28,30 @@ channel and no way in. The cost is about 400 KB compressed in the installer, most of it the vendored editor libraries. +## The page, and the fonts + +The sheet is drawn at the size the document says it is — A4 stays A4, margins +come from its own `sectPr` — and CSS `zoom` scales the whole thing to fit the +window. Scaling rather than widening is the point: a page stretched to the +window would break every line somewhere different from where the printed page +breaks it. The footer carries the control, and **Fit width** is the default +because a page marooned in the middle of a wide window wastes the screen. The +choice is remembered per editor, not per document. + +Ubuntu and Fraunces ship with the add-on, in `fonts/`, because Windows has +neither and a font offered in the ribbon that the machine lacks is a font the +user picks and then cannot see. Regenerate with `npm run fonts`; licences are +in `fonts/LICENSES.txt`. They cost about 700 KB, most of it Ubuntu's Cyrillic +and Greek coverage — dropping those subsets would roughly halve it. + +Two wrinkles worth knowing. The editor is on `file://`, where Chromium +registers `@font-face` rules but refuses to fetch the font files, so the +add-on reads them and hands the page a stylesheet with the woff2 inlined as +data URLs; the same inlined copy goes into the PDF export, whose print window +runs from a temp folder where a relative `url()` would resolve to nothing. If +either path fails the ribbon labels those two families "(not available)" +rather than pretending. + ## The icon `icon.svg` is the one drawing. `npm run icons` rasterises it to diff --git a/bundled-addons/docx-editor/editor.css b/bundled-addons/docx-editor/editor.css index b8900cf..b605090 100644 --- a/bundled-addons/docx-editor/editor.css +++ b/bundled-addons/docx-editor/editor.css @@ -148,10 +148,17 @@ input.rnum { width: 52px; cursor: text; text-align: center; } .banner .btn { border-color: var(--line); background: var(--panel2); } /* ---- document surface ------------------------------------------------ */ +/* The page is drawn at the size the document says it is — A4 stays A4 — + and `zoom` scales the whole thing to fit the window. Scaling rather than + widening is the point: a page stretched to the window would break every + line where the printed page does not. Chromium's `zoom` affects layout, + unlike a transform, so the board scrolls correctly and ProseMirror's + coordinate maths keeps working. */ .board { flex: 1; overflow: auto; background: var(--board); padding: 20px 16px 60px; } -.sheet { max-width: 8.27in; margin: 0 auto; background: var(--paper); +.sheet { width: var(--page-w, 8.27in); margin: 0 auto; background: var(--paper); color: var(--paper-ink); box-shadow: 0 2px 18px var(--paper-edge); - padding: 0.9in 1in; min-height: 60vh; } + padding: var(--page-mt, .9in) var(--page-mr, 1in) var(--page-mb, .9in) var(--page-ml, 1in); + min-height: var(--page-h, auto); zoom: var(--zoom, 1); } .sheet:focus { outline: none; } .sheet .ProseMirror { outline: none; min-height: 50vh; } @@ -196,6 +203,11 @@ input.rnum { width: 52px; cursor: text; text-align: center; } color: var(--ink); border-radius: 5px; cursor: pointer; padding: 3px 7px; font: inherit; font-size: 11px; flex: 0 0 auto; } .footer .fbtn:hover { border-color: rgb(from var(--acid) r g b / .55); } +.footer .zoomer { display: inline-flex; align-items: center; gap: 4px; } +.footer .fsel { border: 1px solid var(--line); background: var(--panel2); color: var(--ink); + border-radius: 5px; font: inherit; font-size: 11px; padding: 2px 4px; + cursor: pointer; } +.footer .fsel:hover { border-color: rgb(from var(--acid) r g b / .55); } /* ---- dialogs --------------------------------------------------------- */ .scrim { position: fixed; inset: 0; background: rgba(4,7,12,.6); z-index: 80; @@ -237,7 +249,9 @@ input.rnum { width: 52px; cursor: text; text-align: center; } .topbar, .ribbon, .footer, .banners, .pop, .scrim, .dropzone { display: none !important; } html, body { height: auto; overflow: visible; background: #fff; } .board { overflow: visible; padding: 0; background: #fff; } - .sheet { box-shadow: none; max-width: none; margin: 0; padding: 0; background: #fff; color: #000; } + /* On paper the page box is the paper; zoom and the screen's margins go. */ + .sheet { box-shadow: none; width: auto; margin: 0; padding: 0; background: #fff; + color: #000; zoom: 1; min-height: 0; } /* Page breaks and widow control come from lib/doc-css.js, which carries its own @media print block so Ctrl+P and the PDF export agree. */ } diff --git a/bundled-addons/docx-editor/editor.html b/bundled-addons/docx-editor/editor.html index 9440aa8..1be4323 100644 --- a/bundled-addons/docx-editor/editor.html +++ b/bundled-addons/docx-editor/editor.html @@ -207,6 +207,19 @@ diff --git a/bundled-addons/docx-editor/editor.js b/bundled-addons/docx-editor/editor.js index a92a607..12f4fca 100644 --- a/bundled-addons/docx-editor/editor.js +++ b/bundled-addons/docx-editor/editor.js @@ -34,10 +34,15 @@ const DE = () => window.DocxEditor; const AUTOSAVE_MS = 20_000; const WORDS_PER_PAGE = 500; +// Ubuntu and Fraunces ship with the add-on (see fonts.css); the rest are +// whatever the machine already has. A font offered in the ribbon but absent +// from the machine is a font the user picks and then cannot see, which is +// why the two that Windows lacks are bundled rather than merely listed. +const BUNDLED_FONTS = ["Ubuntu", "Fraunces"]; const FONTS = [ "Calibri", "Cambria", "Georgia", "Times New Roman", "Arial", "Helvetica", "Verdana", "Tahoma", "Trebuchet MS", "Garamond", "Book Antiqua", - "Courier New", "Consolas", "Segoe UI", + "Courier New", "Consolas", "Segoe UI", "Ubuntu", "Fraunces", ]; const TEXT_COLORS = [ "000000", "404040", "808080", "BFBFBF", "FFFFFF", "C00000", "FF0000", "FFC000", @@ -61,6 +66,12 @@ let autosaveTimer = null; // add-on here and drops the file in Downloads. let savedTarget = null; // {name} — the path itself stays in the add-on +// How large the page is drawn. "fit" and "page" recompute as the window +// changes; a number is a fixed multiplier. Kept per editor rather than per +// document — it is a property of the screen you are looking at, not of the +// file. +let zoomMode = "fit"; + // --- open documents ------------------------------------------------------- // // The editor holds several documents at once, the way the browser holds @@ -744,6 +755,9 @@ function wireRibbon() { $("file-saveas").onclick = () => saveAs(); $("file-pdf").onclick = () => exportPdf(); $("file-print").onclick = () => window.print(); + $("zoom").onchange = (e) => setZoom(e.target.value); + $("zoom-in").onclick = () => stepZoom(1); + $("zoom-out").onclick = () => stepZoom(-1); $("about").onclick = openAbout; $("discard").onclick = () => closeTab(); $("open-folder").onclick = async () => { @@ -769,6 +783,37 @@ function wireRibbon() { // lives in lib/doc-css.js rather than editor.css. Injecting it here (rather // than linking a second stylesheet) keeps the export and the screen reading // from one string. +// The bundled webfonts, fetched from the add-on with the woff2 already +// inlined as data URLs. +// +// A plain to fonts.css does not work here: the page is on file://, +// where Chromium registers the @font-face rules but refuses to fetch the +// font files themselves — the family appears in document.fonts and every +// glyph still comes out in the fallback face. Data URLs sidestep the fetch +// entirely, and the add-on is the only side that can read the files. +async function installFontCss() { + try { + const res = await window.silentmode?.invoke("fontCss", {}); + if (res && res.css) { + const el = document.createElement("style"); + el.id = "bundled-fonts"; + el.textContent = res.css; + document.head.append(el); + await document.fonts.ready; + return; + } + } catch (e) { + console.warn("[docx-editor] bundled fonts unavailable:", e); + } + // Without them, a document that asks for Ubuntu or Fraunces still saves + // correctly — Word will use its own copy — it just isn't drawn faithfully + // here, so say so rather than letting the ribbon imply otherwise. + for (const name of BUNDLED_FONTS) { + const opt = [...$("font-family").options].find((o) => o.value === name); + if (opt) opt.textContent = name + " (not available)"; + } +} + function installDocCss() { const el = document.createElement("style"); el.id = "doc-css"; @@ -785,6 +830,77 @@ function syncPageRule() { document.head.append(el); } el.textContent = DE().docCss.pageRule(docSetup); + syncPageGeometry(); + applyZoom(); +} + +// The sheet is drawn at the size the document claims, margins included, so +// what is on screen is the page rather than a generic rectangle. +function pageInches() { + const page = (docSetup && docSetup.page) || {}; + const size = page.size || {}; + const m = page.margin || {}; + const inch = (twips, fallback) => { + const n = Number(twips); + return Number.isFinite(n) && n > 0 ? n / 1440 : fallback; + }; + return { + w: inch(size.width, 8.27), h: inch(size.height, 11.69), + top: inch(m.top, 1), right: inch(m.right, 1), + bottom: inch(m.bottom, 1), left: inch(m.left, 1), + }; +} + +function syncPageGeometry() { + const p = pageInches(); + const root = document.documentElement.style; + root.setProperty("--page-w", p.w + "in"); + root.setProperty("--page-h", p.h + "in"); + root.setProperty("--page-mt", p.top + "in"); + root.setProperty("--page-mr", p.right + "in"); + root.setProperty("--page-mb", p.bottom + "in"); + root.setProperty("--page-ml", p.left + "in"); +} + +const ZOOM_MIN = 0.4, ZOOM_MAX = 4; + +function applyZoom() { + const board = $("board"); + if (!board) return; + const p = pageInches(); + const style = getComputedStyle(board); + const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + const availW = board.clientWidth - padding - 2; + const availH = board.clientHeight - parseFloat(style.paddingTop) - 24; + + let z; + if (zoomMode === "fit") z = availW / (p.w * 96); + else if (zoomMode === "page") z = Math.min(availW / (p.w * 96), availH / (p.h * 96)); + else z = parseFloat(zoomMode) || 1; + + z = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, z)); + document.documentElement.style.setProperty("--zoom", String(z)); + const sel = $("zoom"); + if (sel && sel.value !== zoomMode) sel.value = zoomMode; + return z; +} + +function setZoom(mode) { + zoomMode = mode; + applyZoom(); + try { window.silentmode?.storage?.set("zoom", mode); } catch {} + if (view) view.focus(); +} + +// Stepping from a fit mode starts at whatever that fit worked out to, so the +// first click doesn't jump somewhere unrelated to what is on screen. +function stepZoom(dir) { + const steps = [0.5, 0.75, 1, 1.25, 1.5, 2, 3]; + const now = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--zoom")) || 1; + const next = dir > 0 + ? steps.find((v) => v > now + 0.01) + : [...steps].reverse().find((v) => v < now - 0.01); + if (next) setZoom(String(next)); } // The document as a standalone HTML page: ProseMirror's own DOM serialisation @@ -829,6 +945,10 @@ function buildPlugins() { "Mod-Tab": () => { cycleDoc(1); return true; }, "Shift-Mod-Tab": () => { cycleDoc(-1); return true; }, "Mod-p": () => { window.print(); return true; }, + "Mod-=": () => { stepZoom(1); return true; }, + "Mod-+": () => { stepZoom(1); return true; }, + "Mod--": () => { stepZoom(-1); return true; }, + "Mod-0": () => { setZoom("fit"); return true; }, "Mod-Enter": (state, dispatch) => { if (dispatch) dispatch(state.tr.replaceSelectionWith(schema.nodes.page_break.create()).scrollIntoView()); return true; @@ -1440,8 +1560,24 @@ async function boot() { } schema = DE().schema.build(); installDocCss(); + try { + const saved = await window.silentmode?.storage?.get("zoom", "fit"); + if (saved) zoomMode = String(saved); + } catch {} syncPageRule(); wireRibbon(); + // After wireRibbon, which is what fills the font menu this may have to + // annotate. + await installFontCss(); + + // A fit mode is a statement about the window, so it has to be recomputed + // when the window changes. + let resizeTimer = null; + window.addEventListener("resize", () => { + if (zoomMode !== "fit" && zoomMode !== "page") return; + clearTimeout(resizeTimer); + resizeTimer = setTimeout(applyZoom, 60); + }); // Whole-window drop target, so dropping a file anywhere works and not // only over the page surface. diff --git a/bundled-addons/docx-editor/fonts.css b/bundled-addons/docx-editor/fonts.css new file mode 100644 index 0000000..47231a2 --- /dev/null +++ b/bundled-addons/docx-editor/fonts.css @@ -0,0 +1,162 @@ +/* Bundled with the Word editor — see addon-build/docx-editor/fetch-fonts.mjs. + Regenerate with `npm run fonts`; do not edit by hand. */ +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-cyrillic-italic-400.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-greek-italic-400.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-latin-ext-italic-400.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-latin-italic-400.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-cyrillic-italic-700.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-greek-italic-700.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-latin-ext-italic-700.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: italic; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-latin-italic-700.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-cyrillic-normal-400.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-greek-normal-400.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-latin-ext-normal-400.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 400; + font-display: block; + src: url('fonts/ubuntu-latin-normal-400.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-cyrillic-normal-700.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-greek-normal-700.woff2') format('woff2'); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-latin-ext-normal-700.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Ubuntu'; + font-style: normal; + font-weight: 700; + font-display: block; + src: url('fonts/ubuntu-latin-normal-700.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Fraunces'; + font-style: italic; + font-weight: 100 900; + font-display: block; + src: url('fonts/fraunces-latin-ext-italic-100_900.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Fraunces'; + font-style: italic; + font-weight: 100 900; + font-display: block; + src: url('fonts/fraunces-latin-italic-100_900.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url('fonts/fraunces-latin-ext-normal-100_900.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Fraunces'; + font-style: normal; + font-weight: 100 900; + font-display: block; + src: url('fonts/fraunces-latin-normal-100_900.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/bundled-addons/docx-editor/fonts/LICENSES.txt b/bundled-addons/docx-editor/fonts/LICENSES.txt new file mode 100644 index 0000000..5bb9cbc --- /dev/null +++ b/bundled-addons/docx-editor/fonts/LICENSES.txt @@ -0,0 +1,211 @@ +Fonts bundled with the Theseus Word editor +========================================== + +Both families are redistributable and are shipped unmodified, as woff2 +subsets built by Google Fonts. Regenerate with: + + cd TheseusNavigator/addon-build/docx-editor && npm run fonts + +They are bundled rather than fetched at runtime: an extension in a browser +built around not phoning home should not ask a font CDN what a document +looks like every time one is opened. + +--- Ubuntu — Ubuntu Font Licence 1.0 --- +Copyright 2010-2011 Canonical Ltd. +https://fonts.google.com/specimen/Ubuntu/license + +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. + + +--- Fraunces — SIL Open Font License 1.1 --- +Copyright 2019 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces) +https://fonts.google.com/specimen/Fraunces/license + +Copyright 2018 The Fraunces Project Authors (https://github.com/undercasetype/Fraunces) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/bundled-addons/docx-editor/fonts/fraunces-latin-ext-italic-100_900.woff2 b/bundled-addons/docx-editor/fonts/fraunces-latin-ext-italic-100_900.woff2 new file mode 100644 index 0000000..2e65049 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/fraunces-latin-ext-italic-100_900.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/fraunces-latin-ext-normal-100_900.woff2 b/bundled-addons/docx-editor/fonts/fraunces-latin-ext-normal-100_900.woff2 new file mode 100644 index 0000000..b6e3e3a Binary files /dev/null and b/bundled-addons/docx-editor/fonts/fraunces-latin-ext-normal-100_900.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/fraunces-latin-italic-100_900.woff2 b/bundled-addons/docx-editor/fonts/fraunces-latin-italic-100_900.woff2 new file mode 100644 index 0000000..b2cf648 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/fraunces-latin-italic-100_900.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/fraunces-latin-normal-100_900.woff2 b/bundled-addons/docx-editor/fonts/fraunces-latin-normal-100_900.woff2 new file mode 100644 index 0000000..cb295bf Binary files /dev/null and b/bundled-addons/docx-editor/fonts/fraunces-latin-normal-100_900.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-400.woff2 new file mode 100644 index 0000000..19bf006 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-700.woff2 new file mode 100644 index 0000000..a5cf35f Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-italic-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-400.woff2 new file mode 100644 index 0000000..bac5b8c Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-700.woff2 new file mode 100644 index 0000000..0c3ccb2 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-cyrillic-normal-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-400.woff2 new file mode 100644 index 0000000..466ae65 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-700.woff2 new file mode 100644 index 0000000..7e0bb1b Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-greek-italic-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-400.woff2 new file mode 100644 index 0000000..aa5ff25 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-700.woff2 new file mode 100644 index 0000000..bb2b4c2 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-greek-normal-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-400.woff2 new file mode 100644 index 0000000..88658c1 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-700.woff2 new file mode 100644 index 0000000..d9c3ca4 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-italic-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-400.woff2 new file mode 100644 index 0000000..16afeec Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-700.woff2 new file mode 100644 index 0000000..a0d4e55 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-ext-normal-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-400.woff2 new file mode 100644 index 0000000..2b557a7 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-700.woff2 new file mode 100644 index 0000000..58b7be7 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-italic-700.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-400.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-400.woff2 new file mode 100644 index 0000000..8cb3d32 Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-400.woff2 differ diff --git a/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-700.woff2 b/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-700.woff2 new file mode 100644 index 0000000..8eaed1f Binary files /dev/null and b/bundled-addons/docx-editor/fonts/ubuntu-latin-normal-700.woff2 differ diff --git a/bundled-addons/docx-editor/index.js b/bundled-addons/docx-editor/index.js index 44d0bfd..bb9c91f 100644 --- a/bundled-addons/docx-editor/index.js +++ b/bundled-addons/docx-editor/index.js @@ -311,11 +311,38 @@ module.exports = { // // A temp file rather than a data: URL: a document with a few photographs // in it runs to megabytes, and long data: URLs get truncated. + // The bundled webfonts, as @font-face rules with the woff2 inlined. + // The print page is loaded from a temp folder, so a relative url() would + // resolve to nothing there — and a PDF whose text falls back to a + // different typeface is exactly the drift the shared stylesheet was + // meant to prevent. Read once, kept for the session. + let inlinedFontCss = null; + function fontCss() { + if (inlinedFontCss !== null) return inlinedFontCss; + try { + const css = fs.readFileSync(path.join(api.folder, "fonts.css"), "utf8"); + inlinedFontCss = css.replace(/url\('fonts\/([^']+)'\)/g, (whole, file) => { + try { + const b64 = fs.readFileSync(path.join(api.folder, "fonts", file)).toString("base64"); + return `url('data:font/woff2;base64,${b64}')`; + } catch { return whole; } + }); + } catch (e) { + api.log("no bundled fonts to inline:", e?.message); + inlinedFontCss = ""; + } + return inlinedFontCss; + } + + api.onMessage("fontCss", () => ({ css: fontCss() })); + api.onMessage("renderPdf", async (payload) => { const { BrowserWindow } = api.require("electron"); const p = payload || {}; - const html = String(p.html || ""); + let html = String(p.html || ""); if (!html) throw new Error("no document to render"); + const fonts = fontCss(); + if (fonts) html = html.replace("", ``); const tmp = path.join(scratchDir, `print-${Date.now()}.html`); fs.writeFileSync(tmp, html, "utf8");