| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191 |
- /*
- ArozOS Office - PDF core
- ========================
- The parts of a browser-side PDF exporter that do not care what kind of
- document is being drawn, shared by Slides (slides/slides_pdf.js) and
- Docs (docs/docs_pdf.js). Both draw the very DOM their editor shows, so
- both need the same answers to the same questions: which font a PDF can
- show a character in, where the browser put a line of text and its
- baseline, how a picture gets embedded once, and what to do with the one
- element nothing else can express.
- The notes on each part were written for Slides and still hold word for
- word for Docs; "slide" there reads as "page".
- Requires pdf-lib (PDFLib) and OfficeFonts (common/fonts.js); fontkit is
- fetched on the first export.
- */
- var OfficePdfCore = (function () {
- "use strict";
- var PX_TO_PT = 0.75;
- var RASTER_SCALE = 3; // device pixels per css px for a fallback raster
- /* ---------------- fonts ---------------- */
- // where fontkit lives; it is fetched only when an export runs
- var FONTKIT_URL = "../common/lib/fontkit.umd.min.js";
- /* Families that are metric-compatible with a standard PDF font, so
- text set in them lands in exactly the same place as on screen. */
- var STD_FAMILIES = {
- "helvetica": "Helvetica", "arial": "Helvetica", "liberation sans": "Helvetica",
- "arimo": "Helvetica", "sans-serif": "Helvetica", "nimbus sans": "Helvetica",
- "times": "TimesRoman", "times new roman": "TimesRoman", "serif": "TimesRoman",
- "liberation serif": "TimesRoman", "tinos": "TimesRoman", "nimbus roman": "TimesRoman",
- "courier": "Courier", "courier new": "Courier", "monospace": "Courier",
- "liberation mono": "Courier", "cousine": "Courier", "nimbus mono": "Courier"
- };
- var STD_VARIANTS = {
- Helvetica: ["Helvetica", "HelveticaBold", "HelveticaOblique", "HelveticaBoldOblique"],
- TimesRoman: ["TimesRoman", "TimesRomanBold", "TimesRomanItalic", "TimesRomanBoldItalic"],
- Courier: ["Courier", "CourierBold", "CourierOblique", "CourierBoldOblique"]
- };
- var GENERICS = { "sans-serif": 1, "serif": 1, "monospace": 1, "cursive": 1, "fantasy": 1 };
- /* WinAnsi is what the standard fonts can encode. A handful of code
- points above U+00FF are in it too, but keeping to the Latin-1 range
- is the rule that can be checked without a table. */
- function winAnsiCp(cp) {
- return cp >= 32 && cp <= 255;
- }
- /* Characters that draw nothing: zero-width spaces and joiners,
- direction marks, variation selectors, the byte order mark, a soft
- hyphen. The editors write a zero-width space into every empty line
- to give it its height, so these are everywhere - and offering one to
- the font stack found no face with a glyph for it, which used to send
- the whole text box or table to a picture. They are simply not drawn. */
- function ignorableCp(cp) {
- return cp === 0xAD || cp === 0x34F || cp === 0x61C ||
- (cp >= 0x180B && cp <= 0x180F) || (cp >= 0x200B && cp <= 0x200F) ||
- (cp >= 0x202A && cp <= 0x202E) || (cp >= 0x2060 && cp <= 0x206F) ||
- (cp >= 0xFE00 && cp <= 0xFE0F) || cp === 0xFEFF ||
- (cp >= 0xE0000 && cp <= 0xE0FFF);
- }
- // visibleText is the text with the characters that draw nothing removed
- function visibleText(text) {
- var out = "";
- for (var i = 0; i < text.length; i++) {
- var cp = text.codePointAt(i);
- if (cp > 0xFFFF) i++;
- if (!ignorableCp(cp)) out += String.fromCodePoint(cp);
- }
- return out;
- }
- /* text-transform is applied by the browser when it paints, so the text
- node still holds what was typed: an all-caps title says "Agenda" in
- the DOM and AGENDA on the slide. The PDF has to apply it the same way. */
- var capitalizeRe = null;
- try {
- // a word starts after anything that is not a letter, a digit or an
- // apostrophe ("don't" stays one word)
- capitalizeRe = new RegExp("(^|[^\\p{L}\\p{N}'\\u2019])(\\p{Ll})", "gu");
- } catch (e) { /* no Unicode property escapes: capitalize is left as typed */ }
- function transformText(text, tt) {
- if (tt === "uppercase") return text.toUpperCase();
- if (tt === "lowercase") return text.toLowerCase();
- if (tt === "capitalize" && capitalizeRe) {
- return text.replace(capitalizeRe, function (m, a, b) { return a + b.toUpperCase(); });
- }
- return text;
- }
- /* haveFamily reports whether a family is actually installed. It has to
- be measured: document.fonts.check() answers "is it loaded", and for a
- local family Chrome says yes whatever name you give it. The reliable
- test is the old one - render a probe string backed by two different
- generics; a family that exists overrides both and comes out a
- different width from each, one that does not falls through to the
- generic and matches it exactly.
- This matters because a deck may ask for a font the machine does not
- have: the browser laid the text out in the fallback, so the fallback
- is what the PDF must match - and that may well be one it can show. */
- var PROBE = "mmmmmmmmwwwwwwwwiiiiiiiil1I0Oo";
- var familyKnown = {};
- var probeCtx = null;
- var probeBase = null;
- function haveFamily(name) {
- if (familyKnown[name] !== undefined) return familyKnown[name];
- try {
- if (!probeCtx) {
- probeCtx = document.createElement("canvas").getContext("2d");
- probeBase = {};
- ["monospace", "serif"].forEach(function (g) {
- probeCtx.font = '72px ' + g;
- probeBase[g] = probeCtx.measureText(PROBE).width;
- });
- }
- var found = true;
- ["monospace", "serif"].forEach(function (g) {
- probeCtx.font = '72px "' + name.replace(/"/g, "") + '", ' + g;
- if (probeCtx.measureText(PROBE).width === probeBase[g]) found = false;
- });
- familyKnown[name] = found;
- } catch (e) {
- familyKnown[name] = true;
- }
- return familyKnown[name];
- }
- /* familyList turns a computed font-family into the list the browser
- walks, with the shipped faces on the end. The tail matters for old
- content: a <font face="..."> names one family and nothing else, and
- without it a character that family has no glyph for would have
- nowhere to go. */
- function familyList(cssFamily) {
- var out = [], seen = {};
- function add(n) {
- n = String(n).trim().replace(/^["']|["']$/g, "");
- if (!n || seen[n.toLowerCase()]) return;
- seen[n.toLowerCase()] = true;
- out.push(n);
- }
- String(cssFamily || "").split(",").forEach(add);
- OfficeFonts.FALLBACK.forEach(add);
- return out;
- }
- /* fontkit is what lets pdf-lib embed a font file of our own. It is the
- largest script the app has and only an export needs it, so it is
- fetched on the first export and not before. */
- var fontkitPromise = null;
- function loadFontkit() {
- if (fontkitPromise) return fontkitPromise;
- if (window.fontkit) {
- fontkitPromise = Promise.resolve(window.fontkit);
- return fontkitPromise;
- }
- fontkitPromise = new Promise(function (resolve, reject) {
- var el = document.createElement("script");
- el.src = FONTKIT_URL;
- el.onload = function () {
- if (window.fontkit) resolve(window.fontkit);
- else reject(new Error("the font toolkit did not load"));
- };
- el.onerror = function () { reject(new Error("the font toolkit did not load")); };
- document.head.appendChild(el);
- });
- return fontkitPromise;
- }
- /* makeFonts is the document's font supply.
- A standard font is there for the asking. A shipped face goes through
- two stages, and the split matters:
- want() fetches the file and parses it, which is what answers "does
- this face have a glyph for this character". Asynchronous,
- so a slide says what it needs, waits (ready), then draws.
- use() puts it in the PDF. Only faces that really get drawn with
- may be embedded: the embedder subsets a font down to the
- glyphs that were asked of it, and a subset of nothing is
- not a font any more - a CFF one fails outright on save.
- Drawing itself stays synchronous, which is what lets a fragment be
- measured and placed in one pass. */
- function makeFonts(pdfDoc, fontkit) {
- var std = {};
- var faces = {}; // url -> { kit, font? }, or null when it failed
- var asked = {}; // url -> Promise, set the moment it is wanted
- var wanted = [];
- function stdFont(family, bold, italic) {
- var names = STD_VARIANTS[family] || STD_VARIANTS.Helvetica;
- var key = names[(bold ? 1 : 0) + (italic ? 2 : 0)];
- if (!std[key]) {
- std[key] = pdfDoc.embedStandardFont(PDFLib.StandardFonts[key]);
- // what a recorded drawing names the font by (pdfdraw.js)
- std[key].__ref = "k:" + key;
- }
- return std[key];
- }
- function want(family, bold, italic) {
- var face = OfficeFonts.faceFor(family, bold, italic);
- if (!face || asked[face.url]) return;
- asked[face.url] = fetch(face.url).then(function (r) {
- if (!r.ok) throw new Error("cannot read " + face.url);
- return r.arrayBuffer();
- }).then(function (buf) {
- var bytes = new Uint8Array(buf);
- faces[face.url] = { bytes: bytes, kit: fontkit.create(bytes) };
- }, function () {
- // a font that will not load is not a reason to fail the
- // export: the next family in the stack gets the character
- faces[face.url] = null;
- });
- wanted.push(asked[face.url]);
- }
- function use(family, bold, italic) {
- var face = OfficeFonts.faceFor(family, bold, italic);
- if (!face) return;
- var rec = faces[face.url];
- if (!rec || rec.font || rec.embedding) return;
- // no standard ligatures, as on screen (the page CSS turns
- // them off: Office never joins "ti" or "fi")
- rec.embedding = pdfDoc.embedFont(rec.bytes, { subset: true, features: { liga: false } })
- .then(function (font) {
- font.__ref = "f:" + absoluteUrl(face.url);
- rec.font = font;
- });
- wanted.push(rec.embedding);
- }
- function ready() {
- var all = wanted;
- wanted = [];
- if (!all.length) return Promise.resolve();
- return Promise.all(all).then(function () { });
- }
- // shipped hands back a loaded face, null while it is not there
- function shipped(family, bold, italic) {
- var face = OfficeFonts.faceFor(family, bold, italic);
- if (!face) return null;
- var rec = faces[face.url];
- if (!rec) return null;
- return {
- font: rec.font, kit: rec.kit,
- synthBold: face.synthBold, synthItalic: face.synthItalic
- };
- }
- // tried says whether asking again could still change the answer
- function tried(family, bold, italic) {
- var face = OfficeFonts.faceFor(family, bold, italic);
- return !face || !!asked[face.url];
- }
- return {
- std: stdFont, want: want, use: use,
- ready: ready, shipped: shipped, tried: tried
- };
- }
- function absoluteUrl(u) {
- try { return new URL(u, document.baseURI).href; } catch (e) { return u; }
- }
- /* resolveChar walks a font stack the way the browser does and says what
- the PDF can put this one character in:
- { std } one of the 14 standard fonts
- { shipped } a face the suite ships, already embedded
- { need } a shipped face that is named but not loaded yet, so
- the answer is not known until it is
- null nothing here can show this character
- bold: true on any of them means the face is bold whatever the run
- asks for: a weighted family name ("Open Sans SemiBold") that the
- screen draws from its base family's bold file (OfficeFonts.substitute).
- A family that is neither - a system font - is stepped over rather
- than used: its bytes are unreadable, so the character goes to the
- next entry, which is the shipped face for its script. */
- function resolveChar(cp, names, fonts, bold, italic) {
- for (var i = 0; i < names.length; i++) {
- var name = names[i];
- var key = name.toLowerCase();
- if (OfficeFonts.isShipped(name)) {
- var rec = fonts.shipped(name, bold, italic);
- if (!rec) {
- if (!fonts.tried(name, bold, italic)) return { need: name };
- continue;
- }
- if (rec.kit && rec.kit.hasGlyphForCodePoint &&
- !rec.kit.hasGlyphForCodePoint(cp)) continue;
- return { shipped: name };
- }
- if (STD_FAMILIES[key] && (GENERICS[key] || haveFamily(name)) && winAnsiCp(cp)) {
- return { std: STD_FAMILIES[key] };
- }
- // a family this machine lacks was drawn in a stand-in of the
- // same kind (OfficeFonts.substitute): the standard font of that
- // kind is far nearer to it than the shipped sans further down
- // the stack, and the fragment is fitted to the browser's width
- var sub = OfficeFonts.substituteOf ? OfficeFonts.substituteOf(name) : null;
- // a metric twin the suite ships stood in on screen: the PDF
- // embeds that same face
- var sb = !!(sub && sub.heavy);
- if (sub && sub.twin) {
- var tw = fonts.shipped(sub.via, bold || sb, italic);
- if (!tw) {
- if (!fonts.tried(sub.via, bold || sb, italic)) return { need: sub.via, bold: sb };
- } else if (!(tw.kit && tw.kit.hasGlyphForCodePoint && !tw.kit.hasGlyphForCodePoint(cp))) {
- return { shipped: sub.via, bold: sb };
- }
- }
- if (sub && winAnsiCp(cp)) {
- var std = STD_FAMILIES[String(sub.via).toLowerCase()] ||
- { serif: "TimesRoman", mono: "Courier" }[sub.kind] || "Helvetica";
- return { std: std, bold: sb };
- }
- }
- return null;
- }
- /* segmentText cuts a fragment into the pieces that share one font, the
- way a browser does per character. Returns null when any character has
- nowhere to go, which is the signal to rasterize instead; an empty list
- when nothing in it draws anything. */
- function segmentText(text, names, fonts, bold, italic) {
- var segs = [], cur = null;
- for (var i = 0; i < text.length; i++) {
- var cp = text.codePointAt(i);
- var ch = String.fromCodePoint(cp);
- if (ch.length > 1) i++; // a surrogate pair
- if (ignorableCp(cp)) continue;
- var res = resolveChar(cp, names, fonts, bold, italic);
- if (!res || res.need) return null;
- var key = (res.std ? "s:" + res.std : "f:" + res.shipped) + (res.bold ? ":b" : "");
- if (cur && cur.key === key) cur.text += ch;
- else { cur = { key: key, res: res, text: ch }; segs.push(cur); }
- }
- return segs;
- }
- // faceOf: the font a resolved piece is drawn in, and what of its style
- // has to be synthesized (family is the name the page declared it under)
- function faceOf(res, fonts, bold, italic) {
- bold = bold || !!res.bold;
- if (res.std) return { font: fonts.std(res.std, bold, italic), synthBold: false, synthItalic: false };
- var rec = fonts.shipped(res.shipped, bold, italic);
- if (!rec) return null;
- var face = OfficeFonts.faceFor(res.shipped, bold, italic);
- return {
- font: rec.font, synthBold: rec.synthBold, synthItalic: rec.synthItalic,
- family: face ? face.family : res.shipped
- };
- }
- /* Where the baseline sits is the browser's decision, and the exporter
- has to ask rather than compute: a system font's metrics are not
- readable from the page, and even for a font that is, the numbers the
- file states are not always the ones the browser uses.
- A canvas answers it. measureText reports the ascent and descent the
- browser resolved for a font stack, which is exactly what it used to
- lay the text out - so the two cannot drift apart.
- This is also why the run's own rect is the reference: the rects a
- Range hands back for text are the content box, ascent plus descent
- tall, not the line box. The baseline is therefore an ascent below the
- top of the rect, with the halving below for the case where a browser
- hands back the taller box instead. */
- var metricsCache = {};
- var metricsCtx = null;
- function fontMetricsOf(cs) {
- var font = cs.fontStyle + " " + cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
- if (metricsCache[font]) return metricsCache[font];
- var size = parseFloat(cs.fontSize) || 12;
- var m = null;
- try {
- if (!metricsCtx) metricsCtx = document.createElement("canvas").getContext("2d");
- metricsCtx.font = font;
- var tm = metricsCtx.measureText("Hxg");
- if (tm && tm.fontBoundingBoxAscent !== undefined) {
- m = { ascent: tm.fontBoundingBoxAscent, descent: tm.fontBoundingBoxDescent };
- }
- } catch (e) { /* fall through to the estimate */ }
- // a browser without the font bounding box: the usual proportions
- if (!m) m = { ascent: size * 0.9, descent: size * 0.22 };
- metricsCache[font] = m;
- return m;
- }
- /* ---------------- synthetic bold and italic ----------------
- A face the suite ships without a bold (the CJK ones) or an italic is
- emboldened or slanted by the browser, and how much is the browser's
- business: Chrome strokes the outline by 1/24 to 1/32 of the size,
- Firefox smears it sideways, other engines differ again. A fixed
- stroke in the PDF was therefore heavier than the screen in one
- browser and lighter in the next, so the amount is measured rather
- than assumed.
- Bold: the run's text is drawn on a canvas in the weight it asked for
- (which the browser synthesizes exactly as it does on the page) and in
- regular weight filled and stroked with a growing pen - the very thing
- the PDF does with text rendering mode 2 - until the two carry the
- same ink. Both drawings go through the same rasterizer, so its
- anti-aliasing cancels out; in Chrome this lands on Skia's own figure
- to the hundredth of a pixel at every size.
- Italic: the slant moves every bit of ink sideways in proportion to
- its height above the baseline, so the shift of the ink's centre over
- the height of that centre is the slant. A browser that does not
- synthesize at all measures as no stroke and no slant, and the PDF
- follows it. */
- var SYNTH_K = 2; // device pixels per css px while measuring
- var synthCache = {};
- var synthCtx = null;
- function synthCanvas(w, h) {
- if (!synthCtx) {
- var c = document.createElement("canvas");
- synthCtx = c.getContext("2d", { willReadFrequently: true });
- }
- var cv = synthCtx.canvas;
- if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; }
- return synthCtx;
- }
- function quoteFamily(f) {
- return '"' + String(f).replace(/["\\]/g, "") + '"';
- }
- // skiaBold is Chrome's own figure, used when nothing can be measured
- function skiaBold(size) {
- var t = clamp((size - 9) / 27, 0, 1);
- return size * (1 / 24 + (1 / 32 - 1 / 24) * t);
- }
- function inkStats(g, w, h) {
- var d = g.getImageData(0, 0, w, h).data;
- var sum = 0, sx = 0, sy = 0;
- for (var i = 3, p = 0; i < d.length; i += 4, p++) {
- var a = d[i];
- if (!a) continue;
- sum += a;
- sx += a * (p % w);
- sy += a * ((p / w) | 0);
- }
- return { ink: sum / 255, cx: sum ? sx / sum : 0, cy: sum ? sy / sum : 0 };
- }
- /* synthStyle answers how much the browser emboldens (stroke width, css
- px) and slants (horizontal shift per unit of height) one face at one
- size, for the faces faceFor() says are synthesized. sample is text
- set in that face: what is measured is the same glyphs. */
- function synthStyle(family, weight, italic, sizePx, sample, wantBold, wantItalic) {
- var out = { bold: 0, skew: 0 };
- var text = visibleText(String(sample || "")).replace(/\s+/g, "").substring(0, 12);
- weight = parseInt(weight, 10) || (wantBold ? 700 : 400);
- if (wantBold) out.bold = measureBold(family, weight, italic, sizePx, text);
- if (wantItalic) out.skew = measureSkew(family, weight, text);
- return out;
- }
- function measureBold(family, weight, italic, size, text) {
- var key = "b|" + family + "|" + weight + "|" + (italic ? 1 : 0) + "|" + size.toFixed(2);
- if (synthCache[key] !== undefined) return synthCache[key];
- var w = skiaBold(size);
- try {
- if (!text) throw new Error("nothing to measure");
- var style = italic ? "italic " : "";
- var fam = quoteFamily(family);
- var g = synthCanvas(1, 1);
- g.setTransform(1, 0, 0, 1, 0, 0);
- g.font = style + "400 " + size + "px " + fam;
- var tw = g.measureText(text).width;
- var W = Math.ceil((tw + size * 1.5) * SYNTH_K), H = Math.ceil(size * 2.2 * SYNTH_K);
- // a measurement this large is not worth its cost; Chrome's
- // figure is proportional above 36px, so measure smaller and scale
- var scale = 1;
- if (W * H > 1600000) {
- scale = Math.sqrt(1600000 / (W * H));
- W = Math.ceil(W * scale); H = Math.ceil(H * scale);
- }
- var k = SYNTH_K * scale;
- g = synthCanvas(W, H);
- var draw = function (wt, pen) {
- g.setTransform(1, 0, 0, 1, 0, 0);
- g.clearRect(0, 0, W, H);
- g.setTransform(k, 0, 0, k, 0, 0);
- g.font = style + wt + " " + size + "px " + fam;
- g.fillStyle = "#000";
- g.textBaseline = "alphabetic";
- g.fillText(text, size * 0.5, size * 1.5);
- if (pen > 0) {
- g.strokeStyle = "#000";
- g.lineWidth = pen;
- g.lineJoin = "miter";
- g.miterLimit = 10;
- g.strokeText(text, size * 0.5, size * 1.5);
- }
- return inkStats(g, W, H).ink;
- };
- var base = draw(400, 0);
- var target = draw(weight, 0);
- if (!(base > 0)) throw new Error("no ink");
- if (target <= base * 1.002) {
- w = 0; // this browser does not embolden
- } else {
- var lo = 0, hi = size / 6;
- for (var it = 0; it < 14; it++) {
- var mid = (lo + hi) / 2;
- if (draw(400, mid) < target) lo = mid; else hi = mid;
- }
- w = (lo + hi) / 2;
- }
- } catch (e) { /* keep Chrome's figure */ }
- synthCache[key] = w;
- return w;
- }
- function measureSkew(family, weight, text) {
- var key = "i|" + family + "|" + weight;
- if (synthCache[key] !== undefined) return synthCache[key];
- var s = 0.25; // Chrome's and Safari's slant
- try {
- if (!text) throw new Error("nothing to measure");
- var size = 64;
- var fam = quoteFamily(family);
- var g = synthCanvas(1, 1);
- g.setTransform(1, 0, 0, 1, 0, 0);
- g.font = weight + " " + size + "px " + fam;
- var W = Math.ceil(g.measureText(text).width + size * 2), H = Math.ceil(size * 2);
- var baseY = size * 1.4;
- g = synthCanvas(W, H);
- var draw = function (style) {
- g.setTransform(1, 0, 0, 1, 0, 0);
- g.clearRect(0, 0, W, H);
- g.font = style + weight + " " + size + "px " + fam;
- g.fillStyle = "#000";
- g.textBaseline = "alphabetic";
- g.fillText(text, size * 0.5, baseY);
- return inkStats(g, W, H);
- };
- var up = draw(""), it = draw("italic ");
- var rise = baseY - up.cy;
- if (!(up.ink > 0) || !(rise > 1)) throw new Error("no ink");
- s = clamp((it.cx - up.cx) / rise, 0, 0.5);
- } catch (e) { /* keep the usual slant */ }
- synthCache[key] = s;
- return s;
- }
- /* eachTextNode is the walk both the font pre-pass and the run collector
- make, kept in one place so they cannot disagree about what counts as
- text on the slide. */
- function eachTextNode(rootEl, fn) {
- var walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT, null);
- var node;
- while ((node = walker.nextNode())) {
- var text = node.nodeValue;
- if (!text || !visibleText(text).trim()) continue;
- var parent = node.parentElement;
- if (!parent) continue;
- var cs = window.getComputedStyle(parent);
- if (cs.visibility === "hidden" || cs.display === "none") continue;
- // source newlines and tabs are whitespace the browser already
- // collapsed; they must not reach a font, but the string has to
- // keep its length - the line split indexes back into the node
- fn(node, text.replace(/[\u0000-\u001F\u007F]/g, " "), cs);
- }
- }
- /* ---------------- small helpers ---------------- */
- function px(v) { return v * PX_TO_PT; }
- function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
- /* parseFill returns both halves of a CSS colour: the colour itself and
- its alpha. The alpha matters - a table's header band is a translucent
- wash of the theme accent, and dropping it turns a tint into a slab. */
- function parseFill(css) {
- if (!css) return null;
- var m = /^rgba?\(([^)]+)\)$/i.exec(String(css).trim());
- if (m) {
- var p = m[1].split(",").map(function (x) { return parseFloat(x); });
- var a = p.length >= 4 ? clamp(p[3], 0, 1) : 1;
- if (a === 0) return null;
- return {
- c: PDFLib.rgb(clamp(p[0] / 255, 0, 1), clamp(p[1] / 255, 0, 1), clamp(p[2] / 255, 0, 1)),
- a: a
- };
- }
- var t = String(css).trim();
- var h = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t);
- if (!h) return null;
- var v = h[1];
- var alpha = 1;
- if (v.length === 8) { alpha = parseInt(v.substring(6, 8), 16) / 255; v = v.substring(0, 6); }
- if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2];
- if (alpha === 0) return null;
- var n = parseInt(v, 16);
- return {
- c: PDFLib.rgb(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255),
- a: alpha
- };
- }
- // parseColor is parseFill when only the colour is wanted
- /* contrastOf answers "what colour shows up on this fill" - only used
- for a shape's markings when it has no stroke colour of its own */
- function contrastOf(css) {
- var f = parseFill(css);
- if (!f) return "#333333";
- var lum = 0.299 * f.c.red + 0.587 * f.c.green + 0.114 * f.c.blue;
- return lum > 0.6 ? "#333333" : "#ffffff";
- }
- function parseColor(css) {
- var f = parseFill(css);
- return f ? f.c : null;
- }
- function dataUrlBytes(src) {
- var comma = String(src || "").indexOf(",");
- if (comma < 0) return null;
- var head = src.substring(0, comma);
- if (head.indexOf(";base64") < 0) return null;
- var bin = atob(src.substring(comma + 1));
- var out = new Uint8Array(bin.length);
- for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
- return { bytes: out, mime: (/^data:([^;]+)/.exec(head) || [])[1] || "" };
- }
- /* ---------------- the drawing context ---------------- */
- /* Page is a thin wrapper that flips the y axis once: the editor's
- coordinates run down from the top-left of the slide, a PDF page's run
- up from the bottom-left, and mixing the two up is the single easiest
- way to get an export subtly wrong. */
- function Page(page, pdfDoc, fonts, heightPx) {
- this.p = page;
- this.doc = pdfDoc;
- this.fonts = fonts;
- this.h = heightPx;
- this.gsCache = {};
- }
- Page.prototype.y = function (topPx) { return px(this.h - topPx); };
- Page.prototype.rect = function (x, y, w, h, opts) {
- this.p.drawRectangle({
- x: px(x), y: this.y(y + h), width: px(w), height: px(h),
- color: opts.fill || undefined,
- borderColor: opts.stroke || undefined,
- borderWidth: opts.strokeW ? px(opts.strokeW) : undefined,
- borderDashArray: opts.dash ? [px(opts.strokeW * 3), px(opts.strokeW * 2)] : undefined,
- opacity: opts.fillOpacity !== undefined ? opts.fillOpacity : opts.opacity,
- borderOpacity: opts.strokeOpacity !== undefined ? opts.strokeOpacity : opts.opacity
- });
- };
- // ops pushes raw content-stream operators, which is how the clip paths
- // and the matrices below are expressed
- Page.prototype.ops = function (list) {
- this.p.pushOperators.apply(this.p, list);
- };
- Page.prototype.save = function () { this.ops([PDFLib.pushGraphicsState()]); };
- Page.prototype.restore = function () { this.ops([PDFLib.popGraphicsState()]); };
- // alpha returns the name of an ExtGState for a given opacity, making one
- // only the first time each distinct value is used on this page
- Page.prototype.alpha = function (a) {
- var key = "a" + Math.round(a * 1000);
- if (!this.gsCache[key] && this.p.recordAlpha) {
- // a recording page (pdfdraw.js) makes the state where it is replayed
- this.p.recordAlpha(key, a);
- this.gsCache[key] = true;
- } else if (!this.gsCache[key]) {
- var ref = this.doc.context.register(this.doc.context.obj({
- Type: "ExtGState", ca: a, CA: a
- }));
- this.p.node.setExtGState(PDFLib.PDFName.of(key), ref);
- this.gsCache[key] = true;
- }
- return key;
- };
- /* ---------------- text ---------------- */
- /* A run is one uniformly formatted fragment of a single line, measured
- off the live DOM: its box, its baseline and the style in force. */
- function collectRuns(rootEl, origin) {
- var runs = [];
- eachTextNode(rootEl, function (node, text, cs) {
- // one entry per line box the fragment occupies
- var range = document.createRange();
- range.selectNodeContents(node);
- var rects = Array.prototype.slice.call(range.getClientRects());
- if (!rects.length) return;
- var ls = parseFloat(cs.letterSpacing);
- var shared = {
- origin: origin,
- node: node,
- names: familyList(cs.fontFamily),
- family: cs.fontFamily,
- metrics: fontMetricsOf(cs),
- size: parseFloat(cs.fontSize) || 12,
- weight: parseInt(cs.fontWeight, 10) || (cs.fontWeight === "bold" ? 700 : 400),
- italic: cs.fontStyle === "italic" || cs.fontStyle === "oblique",
- underline: cs.textDecorationLine.indexOf("underline") >= 0,
- strike: cs.textDecorationLine.indexOf("line-through") >= 0,
- transform: cs.textTransform,
- letterSpacing: isFinite(ls) ? ls : 0,
- color: cs.color,
- background: cs.backgroundColor
- };
- splitByLine(node, text, rects).forEach(function (ln) {
- var r = Object.create(shared);
- r.text = ln.text;
- r.rect = ln.rect;
- r.start = ln.start;
- runs.push(r);
- });
- });
- return runs;
- }
- /* splitByLine maps a text node's client rects back onto the substrings
- that produced them, so each line can be drawn at its own baseline.
- Character-by-character is the only reliable way: the browser decides
- where the break went, and only it knows. start is where each line's
- text begins in the node. */
- function splitByLine(node, text, lineRects) {
- if (lineRects.length === 1) {
- return [{ text: text, rect: lineRects[0], start: 0 }];
- }
- var range = document.createRange();
- var out = [];
- var cur = "";
- var curStart = 0;
- var curTop = null;
- var curRect = null;
- for (var i = 0; i < text.length; i++) {
- range.setStart(node, i);
- range.setEnd(node, i + 1);
- var r = range.getBoundingClientRect();
- if (r.width === 0 && r.height === 0) { cur += text[i]; continue; }
- var top = Math.round(r.top * 10) / 10;
- if (curTop === null || Math.abs(top - curTop) < 0.6) {
- if (curTop === null) { curTop = top; curRect = { left: r.left, top: r.top, bottom: r.bottom, right: r.right }; }
- else { curRect.right = Math.max(curRect.right, r.right); }
- cur += text[i];
- } else {
- out.push({ text: cur, rect: curRect, start: curStart });
- cur = text[i];
- curStart = i;
- curTop = top;
- curRect = { left: r.left, top: r.top, bottom: r.bottom, right: r.right };
- }
- }
- if (cur !== "" && curRect) out.push({ text: cur, rect: curRect, start: curStart });
- return out.length ? out : [{ text: text, rect: lineRects[0], start: 0 }];
- }
- // canDrawAsText is the whole fallback decision, in one place: every
- // character of every run has to have a font that can show it
- function canDrawAsText(runs, fonts) {
- for (var i = 0; i < runs.length; i++) {
- var r = runs[i];
- if (!segmentText(transformText(r.text, r.transform), r.names, fonts, r.weight >= 600, r.italic)) return false;
- }
- return true;
- }
- /* planRun decides how one line fragment is drawn: as text wherever a
- font can show it, and as a picture only for the characters nothing
- can (an emoji, a script the suite does not ship). Each piece comes
- back with its place in the line, measured off the DOM, so a picture
- in the middle of a sentence lands between the words around it:
- [{ text, dx, w, fit, raster }]
- dx is from the run's left edge; fit is the width to fit text to, 0
- when the piece begins or ends on a space (the browser collapses
- those, the measurement does not). */
- function planRun(r, fonts) {
- var bold = r.weight >= 600;
- var whole = r.rect.right - r.rect.left;
- var shown = transformText(r.text, r.transform);
- if (segmentText(shown, r.names, fonts, bold, r.italic)) {
- return [{ text: shown, dx: 0, w: whole, fit: /^\s|\s$/.test(r.text) ? 0 : whole, raster: false }];
- }
- // cut where drawability changes; a character that draws nothing
- // (a joiner inside an emoji) stays with what is around it
- var parts = [], cur = null;
- for (var i = 0; i < r.text.length;) {
- var cp = r.text.codePointAt(i), n = cp > 0xFFFF ? 2 : 1;
- var ok;
- if (ignorableCp(cp)) {
- ok = cur ? cur.ok : true;
- } else {
- var shownCp = transformText(String.fromCodePoint(cp), r.transform).codePointAt(0);
- var res = resolveChar(shownCp, r.names, fonts, bold, r.italic);
- ok = !!(res && !res.need);
- }
- if (cur && cur.ok === ok) cur.end = i + n;
- else { cur = { ok: ok, start: i, end: i + n }; parts.push(cur); }
- i += n;
- }
- /* Each piece is measured against the whole line at the same moment,
- so the place comes out right even when the page is no longer at
- the zoom it was collected at (Docs hands the page back before it
- writes its display list). A line whose text node has changed
- since cannot be measured: it goes in whole, as a picture. */
- var range = document.createRange();
- var now = null;
- try {
- range.setStart(r.node, r.start);
- range.setEnd(r.node, r.start + r.text.length);
- now = range.getBoundingClientRect();
- } catch (e) { now = null; }
- if (!now || !(now.width > 0) || r.node.nodeValue.substr(r.start, r.text.length) !== r.text) {
- return [{ text: shown, dx: 0, w: whole, fit: 0, raster: true }];
- }
- var k = whole / now.width;
- var out = [];
- parts.forEach(function (p) {
- var text = r.text.substring(p.start, p.end);
- range.setStart(r.node, r.start + p.start);
- range.setEnd(r.node, r.start + p.end);
- var b = range.getBoundingClientRect();
- var dx = (b.left - now.left) * k, w = b.width * k;
- if (!(w > 0) && !visibleText(text).trim()) return;
- out.push({
- text: transformText(text, r.transform), dx: dx, w: w,
- fit: /^\s|\s$/.test(text) ? 0 : w, raster: !p.ok
- });
- });
- return out;
- }
- /* rasterText draws a piece of a run as a picture of itself, through
- the page's own canvas - which has the page's fonts, so the picture is
- set in the face the screen used, unlike an SVG image of the element.
- Returns { src, x, y, w, h } relative to the piece's left edge and the
- line's top, or null. */
- function rasterText(r, text, w, lineH, baseline) {
- try {
- var pad = Math.ceil(r.size * 0.3);
- var cw = Math.max(1, w) + pad * 2, ch = lineH + pad * 2;
- var k = RASTER_SCALE;
- var c = document.createElement("canvas");
- c.width = Math.ceil(cw * k);
- c.height = Math.ceil(ch * k);
- var g = c.getContext("2d");
- g.scale(k, k);
- g.font = (r.italic ? "italic " : "") + r.weight + " " + r.size + "px " + r.family;
- if (r.letterSpacing && "letterSpacing" in g) g.letterSpacing = r.letterSpacing + "px";
- g.fillStyle = r.color;
- g.textBaseline = "alphabetic";
- g.fillText(text, pad, pad + baseline);
- return { src: c.toDataURL("image/png"), x: -pad, y: -pad, w: cw, h: ch };
- } catch (e) {
- return null;
- }
- }
- /* drawFragment puts one line fragment on the page as real text, in as
- many pieces as it takes fonts to spell it.
- fitPx, when it is given, is the width the browser gave the fragment.
- The text is squeezed or stretched to exactly that with Tz, which
- costs nothing when the PDF font is the one the browser used (the
- ratio is 1) and is what keeps a substituted face - a system font we
- could not embed - from pushing the rest of the line out of place.
- Bold that a shipped face does not have is stroked rather than filled,
- and italic it does not have is slanted, both by as much as the
- browser does it (synthStyle). Neither moves the advance widths, so
- the two stay in step.
- letterSpacing (css px) goes in as character spacing, which is how
- CSS adds it too: after every character, the last one included. */
- function drawFragment(pg, spec) {
- var fonts = pg.fonts;
- var segs = segmentText(spec.text, spec.names, fonts, spec.bold, spec.italic);
- if (!segs || !segs.length) return 0;
- var ls = spec.letterSpacing || 0;
- var total = 0;
- for (var i = 0; i < segs.length; i++) {
- var face = faceOf(segs[i].res, fonts, spec.bold, spec.italic);
- if (!face) return 0;
- segs[i].face = face;
- segs[i].w = face.font.widthOfTextAtSize(segs[i].text, spec.sizePx) +
- (ls ? ls * Array.from(segs[i].text).length : 0);
- if (face.synthBold || face.synthItalic) {
- segs[i].synth = synthStyle(face.family, spec.weight || (spec.bold ? 700 : 400), spec.italic,
- spec.sizePx, segs[i].text, face.synthBold, face.synthItalic);
- }
- total += segs[i].w;
- }
- var scale = 1;
- if (spec.fitPx > 0 && total > 0) {
- var ratio = spec.fitPx / total;
- // a ratio far from 1 means the measurement, not the font, is
- // wrong (a collapsed space, a transform) - leave it alone
- if (ratio > 0.5 && ratio < 2 && Math.abs(ratio - 1) > 0.005) scale = ratio;
- }
- var col = spec.color || PDFLib.rgb(0, 0, 0);
- var cursor = spec.xPx;
- segs.forEach(function (seg) {
- pg.save();
- var ops = [];
- var synth = seg.synth || { bold: 0, skew: 0 };
- if (scale !== 1) ops.push(PDFLib.setCharacterSqueeze(scale * 100));
- if (ls) ops.push(PDFLib.setCharacterSpacing(px(ls)));
- if (synth.bold > 0.01) {
- ops.push(PDFLib.setTextRenderingMode(PDFLib.TextRenderingMode.FillAndOutline));
- ops.push(PDFLib.setLineWidth(px(synth.bold)));
- ops.push(PDFLib.setStrokingColor(col));
- }
- if (ops.length) pg.ops(ops);
- var opt = {
- x: px(cursor), y: pg.y(spec.baselinePx),
- size: px(spec.sizePx), font: seg.face.font, color: col
- };
- // the text matrix's horizontal shear by height (pdf-lib calls
- // that angle ySkew)
- if (synth.skew > 0.001) opt.ySkew = PDFLib.radians(Math.atan(synth.skew));
- pg.p.drawText(seg.text, opt);
- pg.restore();
- cursor += seg.w * scale;
- });
- return total * scale;
- }
- /* drawRuns puts every run on the page at the baseline the browser laid
- it out on, in the width the browser gave it. A character no font can
- show comes in as a picture of itself, in its own place in the line
- (planRun); drawImage(src, x, y, w, h) puts that picture on the page,
- and without it such characters are left out. */
- function drawRuns(pg, runs, drawImage) {
- runs.forEach(function (r) {
- var x = r.origin.x + (r.rect.left - r.origin.left);
- var top = r.origin.y + (r.rect.top - r.origin.top);
- var lineH = r.rect.bottom - r.rect.top;
- var domW = r.rect.right - r.rect.left;
- var glyphH = r.metrics.ascent + r.metrics.descent;
- var baseline = (lineH - glyphH) / 2 + r.metrics.ascent;
- var col = parseColor(r.color) || PDFLib.rgb(0, 0, 0);
- var bg = parseFill(r.background);
- if (bg) pg.rect(x, top, domW, lineH, { fill: bg.c, fillOpacity: bg.a });
- var drawn = 0;
- planRun(r, pg.fonts).forEach(function (pc) {
- if (pc.raster) {
- var pic = drawImage ? rasterText(r, pc.text, pc.w, lineH, baseline) : null;
- if (pic) drawImage(pic.src, x + pc.dx + pic.x, top + pic.y, pic.w, pic.h);
- drawn += pc.w;
- return;
- }
- drawn += drawFragment(pg, {
- text: pc.text, names: r.names,
- bold: r.weight >= 600, weight: r.weight, italic: r.italic,
- sizePx: r.size, xPx: x + pc.dx, baselinePx: top + baseline,
- color: col, fitPx: pc.fit, letterSpacing: r.letterSpacing
- });
- });
- if (r.underline || r.strike) {
- var yOff = r.underline ? baseline + r.size * 0.11 : baseline - r.size * 0.28;
- pg.rect(x, top + yOff, drawn || domW, Math.max(0.7, r.size * 0.06), { fill: col });
- }
- });
- }
- /* ---------------- rasterizing one element ---------------- */
- /* The documented last resort, and it has one rule: the pixels come out
- of a render of the whole slide, then get cropped to the element that
- needed them.
- Rasterizing an element on its own looks tempting and is wrong.
- html2canvas re-renders a *clone*, and a clone torn out of its
- absolutely positioned parent loses the width it was laid out in -
- so the text re-wraps and the export stops matching the editor,
- which is the one thing it must not do. Rendering the slide keeps
- every element in the context it was measured in.
- What lands in the PDF is still only that element's own box: one
- image, at its own position, with everything else on the page a real
- PDF object. */
- /* The picture is taken through an SVG <foreignObject>, which means the
- *browser* lays the element out and paints it - the same engine, the
- same fonts, the same line breaks as the editor.
- This is not the obvious choice, so: html2canvas was tried first and
- is wrong for this. It re-implements layout over a clone, and on
- mixed CJK/Latin text with pre-wrap it breaks lines somewhere else
- than the browser did, which is precisely the failure this whole
- rework exists to remove. A foreignObject cannot re-wrap anything,
- because it is not re-laying anything out.
- The cost is that computed styles have to be inlined onto the clone
- (an SVG image cannot reach the page's stylesheets) and every
- resource must already be a data URL - which, in a slide, it is. */
- /* ext, when given, is the area to capture relative to the element's own
- box ({ x, y, w, h }, x and y usually <= 0): what overflows the box -
- a table whose rows grew past its frame, text running out of its box -
- is on the slide, so it belongs in the picture too. */
- function rasterizeElement(el, w, h, ext) {
- try {
- var clone = el.cloneNode(true);
- inlineStyles(el, clone);
- ext = ext || { x: 0, y: 0, w: w, h: h };
- // the foreignObject supplies the box, so the clone must not
- // carry the absolute placement it had on the slide
- clone.style.position = "absolute";
- clone.style.left = (-ext.x) + "px";
- clone.style.top = (-ext.y) + "px";
- clone.style.transform = "none";
- clone.style.margin = "0";
- clone.style.width = w + "px";
- clone.style.height = h + "px";
- var cw = Math.max(1, Math.ceil(ext.w)), ch = Math.max(1, Math.ceil(ext.h));
- /* The markup inside a foreignObject has to be well-formed XML,
- and innerHTML is not: it writes <br> unclosed, which makes the
- whole SVG fail to parse and the element vanish from the page.
- XMLSerializer writes real XHTML, so it cannot. */
- var wrap = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
- wrap.setAttribute("style", "position:relative;overflow:hidden;width:" + cw + "px;height:" + ch + "px;");
- wrap.appendChild(clone);
- var xhtml = new XMLSerializer().serializeToString(wrap);
- var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + cw +
- '" height="' + ch + '"><foreignObject x="0" y="0" width="' + cw +
- '" height="' + ch + '">' + xhtml + "</foreignObject></svg>";
- var url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
- return new Promise(function (resolve) {
- var img = new Image();
- img.onload = function () {
- try {
- var c = document.createElement("canvas");
- c.width = Math.max(1, Math.round(cw * RASTER_SCALE));
- c.height = Math.max(1, Math.round(ch * RASTER_SCALE));
- var g = c.getContext("2d");
- g.drawImage(img, 0, 0, c.width, c.height);
- resolve(c.toDataURL("image/png"));
- } catch (e) { resolve(null); }
- };
- img.onerror = function () { resolve(null); };
- img.src = url;
- });
- } catch (e) {
- return Promise.resolve(null);
- }
- }
- /* rasterizeFallback is the very last resort, for the case where even
- the foreignObject route fails. html2canvas re-implements layout and
- can break mixed-script lines somewhere the browser did not, so it is
- only ever reached when the alternative is dropping the element
- from the page entirely - which would be worse. */
- function rasterizeFallback(el, w, h) {
- if (typeof html2canvas === "undefined") return Promise.resolve(null);
- return html2canvas(el, {
- scale: RASTER_SCALE, useCORS: true, backgroundColor: null, logging: false
- }).then(function (canvas) {
- return canvas.toDataURL("image/png");
- }).catch(function () { return null; });
- }
- /* inlineStyles copies the computed style of every node in the subtree
- onto the clone, because the SVG image has no access to the page's
- stylesheets. Slide markup is small - a few divs and spans - so
- walking the whole property list is affordable and leaves nothing out. */
- function inlineStyles(src, dst) {
- var cs = window.getComputedStyle(src);
- var out = "";
- for (var i = 0; i < cs.length; i++) {
- var prop = cs[i];
- out += prop + ":" + cs.getPropertyValue(prop) + ";";
- }
- dst.setAttribute("style", out);
- var a = src.children, b = dst.children;
- for (var j = 0; j < a.length && j < b.length; j++) inlineStyles(a[j], b[j]);
- }
- /* ---------------- images ---------------- */
- /* filteredImageData re-encodes a picture through a canvas when it
- carries a colour treatment. Applying a filter is a pixel operation in
- any renderer, so this is the correct way to do it, not a fallback. */
- function filteredImageData(src, filter, naturalW, naturalH) {
- return new Promise(function (resolve) {
- var img = new Image();
- img.onload = function () {
- try {
- var c = document.createElement("canvas");
- c.width = naturalW || img.naturalWidth;
- c.height = naturalH || img.naturalHeight;
- var g = c.getContext("2d");
- g.filter = filter;
- g.drawImage(img, 0, 0, c.width, c.height);
- resolve(c.toDataURL("image/png"));
- } catch (e) { resolve(null); }
- };
- img.onerror = function () { resolve(null); };
- img.src = src;
- });
- }
- /* sniffImage decides which embedder to use from the bytes themselves
- rather than from a mime string, which a fetched file may not carry */
- function sniffImage(bytes) {
- if (bytes.length > 3 && bytes[0] === 0xFF && bytes[1] === 0xD8) return "jpg";
- return "png";
- }
- /* embedImage caches by source string: a deck that uses one picture on
- twenty slides embeds its bytes once.
- A deck keeps its large media out of the body as media?file= links,
- so a source that is not a data URL is fetched. Embedding the
- original bytes is the point - re-encoding through a canvas would
- turn a photo into a much larger lossless PNG. */
- function makeImageEmbedder(pdfDoc) {
- var cache = {};
- function embedBytes(bytes) {
- return sniffImage(bytes) === "jpg" ? pdfDoc.embedJpg(bytes) : pdfDoc.embedPng(bytes);
- }
- return function (src) {
- if (!src) return Promise.resolve(null);
- if (cache[src]) return cache[src];
- var p;
- var d = dataUrlBytes(src);
- if (d) {
- p = /jpe?g/i.test(d.mime) ? pdfDoc.embedJpg(d.bytes) : embedBytes(d.bytes);
- } else {
- p = fetch(src).then(function (r) {
- if (!r.ok) throw new Error("cannot read " + src);
- return r.arrayBuffer();
- }).then(function (buf) {
- return embedBytes(new Uint8Array(buf));
- });
- }
- cache[src] = p.catch(function () { return null; });
- return cache[src];
- };
- }
- return {
- PX_TO_PT: PX_TO_PT,
- RASTER_SCALE: RASTER_SCALE,
- STD_FAMILIES: STD_FAMILIES,
- winAnsiCp: winAnsiCp,
- ignorableCp: ignorableCp,
- visibleText: visibleText,
- transformText: transformText,
- synthStyle: synthStyle,
- haveFamily: haveFamily,
- familyList: familyList,
- loadFontkit: loadFontkit,
- makeFonts: makeFonts,
- resolveChar: resolveChar,
- segmentText: segmentText,
- faceOf: faceOf,
- fontMetricsOf: fontMetricsOf,
- eachTextNode: eachTextNode,
- px: px,
- clamp: clamp,
- parseFill: parseFill,
- parseColor: parseColor,
- contrastOf: contrastOf,
- dataUrlBytes: dataUrlBytes,
- Page: Page,
- collectRuns: collectRuns,
- splitByLine: splitByLine,
- canDrawAsText: canDrawAsText,
- planRun: planRun,
- rasterText: rasterText,
- drawFragment: drawFragment,
- drawRuns: drawRuns,
- rasterizeElement: rasterizeElement,
- rasterizeFallback: rasterizeFallback,
- inlineStyles: inlineStyles,
- filteredImageData: filteredImageData,
- sniffImage: sniffImage,
- makeImageEmbedder: makeImageEmbedder
- };
- })();
|