pdfcore.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. /*
  2. ArozOS Office - PDF core
  3. ========================
  4. The parts of a browser-side PDF exporter that do not care what kind of
  5. document is being drawn, shared by Slides (slides/slides_pdf.js) and
  6. Docs (docs/docs_pdf.js). Both draw the very DOM their editor shows, so
  7. both need the same answers to the same questions: which font a PDF can
  8. show a character in, where the browser put a line of text and its
  9. baseline, how a picture gets embedded once, and what to do with the one
  10. element nothing else can express.
  11. The notes on each part were written for Slides and still hold word for
  12. word for Docs; "slide" there reads as "page".
  13. Requires pdf-lib (PDFLib) and OfficeFonts (common/fonts.js); fontkit is
  14. fetched on the first export.
  15. */
  16. var OfficePdfCore = (function () {
  17. "use strict";
  18. var PX_TO_PT = 0.75;
  19. var RASTER_SCALE = 3; // device pixels per css px for a fallback raster
  20. /* ---------------- fonts ---------------- */
  21. // where fontkit lives; it is fetched only when an export runs
  22. var FONTKIT_URL = "../common/lib/fontkit.umd.min.js";
  23. /* Families that are metric-compatible with a standard PDF font, so
  24. text set in them lands in exactly the same place as on screen. */
  25. var STD_FAMILIES = {
  26. "helvetica": "Helvetica", "arial": "Helvetica", "liberation sans": "Helvetica",
  27. "arimo": "Helvetica", "sans-serif": "Helvetica", "nimbus sans": "Helvetica",
  28. "times": "TimesRoman", "times new roman": "TimesRoman", "serif": "TimesRoman",
  29. "liberation serif": "TimesRoman", "tinos": "TimesRoman", "nimbus roman": "TimesRoman",
  30. "courier": "Courier", "courier new": "Courier", "monospace": "Courier",
  31. "liberation mono": "Courier", "cousine": "Courier", "nimbus mono": "Courier"
  32. };
  33. var STD_VARIANTS = {
  34. Helvetica: ["Helvetica", "HelveticaBold", "HelveticaOblique", "HelveticaBoldOblique"],
  35. TimesRoman: ["TimesRoman", "TimesRomanBold", "TimesRomanItalic", "TimesRomanBoldItalic"],
  36. Courier: ["Courier", "CourierBold", "CourierOblique", "CourierBoldOblique"]
  37. };
  38. var GENERICS = { "sans-serif": 1, "serif": 1, "monospace": 1, "cursive": 1, "fantasy": 1 };
  39. /* WinAnsi is what the standard fonts can encode. A handful of code
  40. points above U+00FF are in it too, but keeping to the Latin-1 range
  41. is the rule that can be checked without a table. */
  42. function winAnsiCp(cp) {
  43. return cp >= 32 && cp <= 255;
  44. }
  45. /* Characters that draw nothing: zero-width spaces and joiners,
  46. direction marks, variation selectors, the byte order mark, a soft
  47. hyphen. The editors write a zero-width space into every empty line
  48. to give it its height, so these are everywhere - and offering one to
  49. the font stack found no face with a glyph for it, which used to send
  50. the whole text box or table to a picture. They are simply not drawn. */
  51. function ignorableCp(cp) {
  52. return cp === 0xAD || cp === 0x34F || cp === 0x61C ||
  53. (cp >= 0x180B && cp <= 0x180F) || (cp >= 0x200B && cp <= 0x200F) ||
  54. (cp >= 0x202A && cp <= 0x202E) || (cp >= 0x2060 && cp <= 0x206F) ||
  55. (cp >= 0xFE00 && cp <= 0xFE0F) || cp === 0xFEFF ||
  56. (cp >= 0xE0000 && cp <= 0xE0FFF);
  57. }
  58. // visibleText is the text with the characters that draw nothing removed
  59. function visibleText(text) {
  60. var out = "";
  61. for (var i = 0; i < text.length; i++) {
  62. var cp = text.codePointAt(i);
  63. if (cp > 0xFFFF) i++;
  64. if (!ignorableCp(cp)) out += String.fromCodePoint(cp);
  65. }
  66. return out;
  67. }
  68. /* text-transform is applied by the browser when it paints, so the text
  69. node still holds what was typed: an all-caps title says "Agenda" in
  70. the DOM and AGENDA on the slide. The PDF has to apply it the same way. */
  71. var capitalizeRe = null;
  72. try {
  73. // a word starts after anything that is not a letter, a digit or an
  74. // apostrophe ("don't" stays one word)
  75. capitalizeRe = new RegExp("(^|[^\\p{L}\\p{N}'\\u2019])(\\p{Ll})", "gu");
  76. } catch (e) { /* no Unicode property escapes: capitalize is left as typed */ }
  77. function transformText(text, tt) {
  78. if (tt === "uppercase") return text.toUpperCase();
  79. if (tt === "lowercase") return text.toLowerCase();
  80. if (tt === "capitalize" && capitalizeRe) {
  81. return text.replace(capitalizeRe, function (m, a, b) { return a + b.toUpperCase(); });
  82. }
  83. return text;
  84. }
  85. /* haveFamily reports whether a family is actually installed. It has to
  86. be measured: document.fonts.check() answers "is it loaded", and for a
  87. local family Chrome says yes whatever name you give it. The reliable
  88. test is the old one - render a probe string backed by two different
  89. generics; a family that exists overrides both and comes out a
  90. different width from each, one that does not falls through to the
  91. generic and matches it exactly.
  92. This matters because a deck may ask for a font the machine does not
  93. have: the browser laid the text out in the fallback, so the fallback
  94. is what the PDF must match - and that may well be one it can show. */
  95. var PROBE = "mmmmmmmmwwwwwwwwiiiiiiiil1I0Oo";
  96. var familyKnown = {};
  97. var probeCtx = null;
  98. var probeBase = null;
  99. function haveFamily(name) {
  100. if (familyKnown[name] !== undefined) return familyKnown[name];
  101. try {
  102. if (!probeCtx) {
  103. probeCtx = document.createElement("canvas").getContext("2d");
  104. probeBase = {};
  105. ["monospace", "serif"].forEach(function (g) {
  106. probeCtx.font = '72px ' + g;
  107. probeBase[g] = probeCtx.measureText(PROBE).width;
  108. });
  109. }
  110. var found = true;
  111. ["monospace", "serif"].forEach(function (g) {
  112. probeCtx.font = '72px "' + name.replace(/"/g, "") + '", ' + g;
  113. if (probeCtx.measureText(PROBE).width === probeBase[g]) found = false;
  114. });
  115. familyKnown[name] = found;
  116. } catch (e) {
  117. familyKnown[name] = true;
  118. }
  119. return familyKnown[name];
  120. }
  121. /* familyList turns a computed font-family into the list the browser
  122. walks, with the shipped faces on the end. The tail matters for old
  123. content: a <font face="..."> names one family and nothing else, and
  124. without it a character that family has no glyph for would have
  125. nowhere to go. */
  126. function familyList(cssFamily) {
  127. var out = [], seen = {};
  128. function add(n) {
  129. n = String(n).trim().replace(/^["']|["']$/g, "");
  130. if (!n || seen[n.toLowerCase()]) return;
  131. seen[n.toLowerCase()] = true;
  132. out.push(n);
  133. }
  134. String(cssFamily || "").split(",").forEach(add);
  135. OfficeFonts.FALLBACK.forEach(add);
  136. return out;
  137. }
  138. /* fontkit is what lets pdf-lib embed a font file of our own. It is the
  139. largest script the app has and only an export needs it, so it is
  140. fetched on the first export and not before. */
  141. var fontkitPromise = null;
  142. function loadFontkit() {
  143. if (fontkitPromise) return fontkitPromise;
  144. if (window.fontkit) {
  145. fontkitPromise = Promise.resolve(window.fontkit);
  146. return fontkitPromise;
  147. }
  148. fontkitPromise = new Promise(function (resolve, reject) {
  149. var el = document.createElement("script");
  150. el.src = FONTKIT_URL;
  151. el.onload = function () {
  152. if (window.fontkit) resolve(window.fontkit);
  153. else reject(new Error("the font toolkit did not load"));
  154. };
  155. el.onerror = function () { reject(new Error("the font toolkit did not load")); };
  156. document.head.appendChild(el);
  157. });
  158. return fontkitPromise;
  159. }
  160. /* makeFonts is the document's font supply.
  161. A standard font is there for the asking. A shipped face goes through
  162. two stages, and the split matters:
  163. want() fetches the file and parses it, which is what answers "does
  164. this face have a glyph for this character". Asynchronous,
  165. so a slide says what it needs, waits (ready), then draws.
  166. use() puts it in the PDF. Only faces that really get drawn with
  167. may be embedded: the embedder subsets a font down to the
  168. glyphs that were asked of it, and a subset of nothing is
  169. not a font any more - a CFF one fails outright on save.
  170. Drawing itself stays synchronous, which is what lets a fragment be
  171. measured and placed in one pass. */
  172. function makeFonts(pdfDoc, fontkit) {
  173. var std = {};
  174. var faces = {}; // url -> { kit, font? }, or null when it failed
  175. var asked = {}; // url -> Promise, set the moment it is wanted
  176. var wanted = [];
  177. function stdFont(family, bold, italic) {
  178. var names = STD_VARIANTS[family] || STD_VARIANTS.Helvetica;
  179. var key = names[(bold ? 1 : 0) + (italic ? 2 : 0)];
  180. if (!std[key]) {
  181. std[key] = pdfDoc.embedStandardFont(PDFLib.StandardFonts[key]);
  182. // what a recorded drawing names the font by (pdfdraw.js)
  183. std[key].__ref = "k:" + key;
  184. }
  185. return std[key];
  186. }
  187. function want(family, bold, italic) {
  188. var face = OfficeFonts.faceFor(family, bold, italic);
  189. if (!face || asked[face.url]) return;
  190. asked[face.url] = fetch(face.url).then(function (r) {
  191. if (!r.ok) throw new Error("cannot read " + face.url);
  192. return r.arrayBuffer();
  193. }).then(function (buf) {
  194. var bytes = new Uint8Array(buf);
  195. faces[face.url] = { bytes: bytes, kit: fontkit.create(bytes) };
  196. }, function () {
  197. // a font that will not load is not a reason to fail the
  198. // export: the next family in the stack gets the character
  199. faces[face.url] = null;
  200. });
  201. wanted.push(asked[face.url]);
  202. }
  203. function use(family, bold, italic) {
  204. var face = OfficeFonts.faceFor(family, bold, italic);
  205. if (!face) return;
  206. var rec = faces[face.url];
  207. if (!rec || rec.font || rec.embedding) return;
  208. // no standard ligatures, as on screen (the page CSS turns
  209. // them off: Office never joins "ti" or "fi")
  210. rec.embedding = pdfDoc.embedFont(rec.bytes, { subset: true, features: { liga: false } })
  211. .then(function (font) {
  212. font.__ref = "f:" + absoluteUrl(face.url);
  213. rec.font = font;
  214. });
  215. wanted.push(rec.embedding);
  216. }
  217. function ready() {
  218. var all = wanted;
  219. wanted = [];
  220. if (!all.length) return Promise.resolve();
  221. return Promise.all(all).then(function () { });
  222. }
  223. // shipped hands back a loaded face, null while it is not there
  224. function shipped(family, bold, italic) {
  225. var face = OfficeFonts.faceFor(family, bold, italic);
  226. if (!face) return null;
  227. var rec = faces[face.url];
  228. if (!rec) return null;
  229. return {
  230. font: rec.font, kit: rec.kit,
  231. synthBold: face.synthBold, synthItalic: face.synthItalic
  232. };
  233. }
  234. // tried says whether asking again could still change the answer
  235. function tried(family, bold, italic) {
  236. var face = OfficeFonts.faceFor(family, bold, italic);
  237. return !face || !!asked[face.url];
  238. }
  239. return {
  240. std: stdFont, want: want, use: use,
  241. ready: ready, shipped: shipped, tried: tried
  242. };
  243. }
  244. function absoluteUrl(u) {
  245. try { return new URL(u, document.baseURI).href; } catch (e) { return u; }
  246. }
  247. /* resolveChar walks a font stack the way the browser does and says what
  248. the PDF can put this one character in:
  249. { std } one of the 14 standard fonts
  250. { shipped } a face the suite ships, already embedded
  251. { need } a shipped face that is named but not loaded yet, so
  252. the answer is not known until it is
  253. null nothing here can show this character
  254. bold: true on any of them means the face is bold whatever the run
  255. asks for: a weighted family name ("Open Sans SemiBold") that the
  256. screen draws from its base family's bold file (OfficeFonts.substitute).
  257. A family that is neither - a system font - is stepped over rather
  258. than used: its bytes are unreadable, so the character goes to the
  259. next entry, which is the shipped face for its script. */
  260. function resolveChar(cp, names, fonts, bold, italic) {
  261. for (var i = 0; i < names.length; i++) {
  262. var name = names[i];
  263. var key = name.toLowerCase();
  264. if (OfficeFonts.isShipped(name)) {
  265. var rec = fonts.shipped(name, bold, italic);
  266. if (!rec) {
  267. if (!fonts.tried(name, bold, italic)) return { need: name };
  268. continue;
  269. }
  270. if (rec.kit && rec.kit.hasGlyphForCodePoint &&
  271. !rec.kit.hasGlyphForCodePoint(cp)) continue;
  272. return { shipped: name };
  273. }
  274. if (STD_FAMILIES[key] && (GENERICS[key] || haveFamily(name)) && winAnsiCp(cp)) {
  275. return { std: STD_FAMILIES[key] };
  276. }
  277. // a family this machine lacks was drawn in a stand-in of the
  278. // same kind (OfficeFonts.substitute): the standard font of that
  279. // kind is far nearer to it than the shipped sans further down
  280. // the stack, and the fragment is fitted to the browser's width
  281. var sub = OfficeFonts.substituteOf ? OfficeFonts.substituteOf(name) : null;
  282. // a metric twin the suite ships stood in on screen: the PDF
  283. // embeds that same face
  284. var sb = !!(sub && sub.heavy);
  285. if (sub && sub.twin) {
  286. var tw = fonts.shipped(sub.via, bold || sb, italic);
  287. if (!tw) {
  288. if (!fonts.tried(sub.via, bold || sb, italic)) return { need: sub.via, bold: sb };
  289. } else if (!(tw.kit && tw.kit.hasGlyphForCodePoint && !tw.kit.hasGlyphForCodePoint(cp))) {
  290. return { shipped: sub.via, bold: sb };
  291. }
  292. }
  293. if (sub && winAnsiCp(cp)) {
  294. var std = STD_FAMILIES[String(sub.via).toLowerCase()] ||
  295. { serif: "TimesRoman", mono: "Courier" }[sub.kind] || "Helvetica";
  296. return { std: std, bold: sb };
  297. }
  298. }
  299. return null;
  300. }
  301. /* segmentText cuts a fragment into the pieces that share one font, the
  302. way a browser does per character. Returns null when any character has
  303. nowhere to go, which is the signal to rasterize instead; an empty list
  304. when nothing in it draws anything. */
  305. function segmentText(text, names, fonts, bold, italic) {
  306. var segs = [], cur = null;
  307. for (var i = 0; i < text.length; i++) {
  308. var cp = text.codePointAt(i);
  309. var ch = String.fromCodePoint(cp);
  310. if (ch.length > 1) i++; // a surrogate pair
  311. if (ignorableCp(cp)) continue;
  312. var res = resolveChar(cp, names, fonts, bold, italic);
  313. if (!res || res.need) return null;
  314. var key = (res.std ? "s:" + res.std : "f:" + res.shipped) + (res.bold ? ":b" : "");
  315. if (cur && cur.key === key) cur.text += ch;
  316. else { cur = { key: key, res: res, text: ch }; segs.push(cur); }
  317. }
  318. return segs;
  319. }
  320. // faceOf: the font a resolved piece is drawn in, and what of its style
  321. // has to be synthesized (family is the name the page declared it under)
  322. function faceOf(res, fonts, bold, italic) {
  323. bold = bold || !!res.bold;
  324. if (res.std) return { font: fonts.std(res.std, bold, italic), synthBold: false, synthItalic: false };
  325. var rec = fonts.shipped(res.shipped, bold, italic);
  326. if (!rec) return null;
  327. var face = OfficeFonts.faceFor(res.shipped, bold, italic);
  328. return {
  329. font: rec.font, synthBold: rec.synthBold, synthItalic: rec.synthItalic,
  330. family: face ? face.family : res.shipped
  331. };
  332. }
  333. /* Where the baseline sits is the browser's decision, and the exporter
  334. has to ask rather than compute: a system font's metrics are not
  335. readable from the page, and even for a font that is, the numbers the
  336. file states are not always the ones the browser uses.
  337. A canvas answers it. measureText reports the ascent and descent the
  338. browser resolved for a font stack, which is exactly what it used to
  339. lay the text out - so the two cannot drift apart.
  340. This is also why the run's own rect is the reference: the rects a
  341. Range hands back for text are the content box, ascent plus descent
  342. tall, not the line box. The baseline is therefore an ascent below the
  343. top of the rect, with the halving below for the case where a browser
  344. hands back the taller box instead. */
  345. var metricsCache = {};
  346. var metricsCtx = null;
  347. function fontMetricsOf(cs) {
  348. var font = cs.fontStyle + " " + cs.fontWeight + " " + cs.fontSize + " " + cs.fontFamily;
  349. if (metricsCache[font]) return metricsCache[font];
  350. var size = parseFloat(cs.fontSize) || 12;
  351. var m = null;
  352. try {
  353. if (!metricsCtx) metricsCtx = document.createElement("canvas").getContext("2d");
  354. metricsCtx.font = font;
  355. var tm = metricsCtx.measureText("Hxg");
  356. if (tm && tm.fontBoundingBoxAscent !== undefined) {
  357. m = { ascent: tm.fontBoundingBoxAscent, descent: tm.fontBoundingBoxDescent };
  358. }
  359. } catch (e) { /* fall through to the estimate */ }
  360. // a browser without the font bounding box: the usual proportions
  361. if (!m) m = { ascent: size * 0.9, descent: size * 0.22 };
  362. metricsCache[font] = m;
  363. return m;
  364. }
  365. /* ---------------- synthetic bold and italic ----------------
  366. A face the suite ships without a bold (the CJK ones) or an italic is
  367. emboldened or slanted by the browser, and how much is the browser's
  368. business: Chrome strokes the outline by 1/24 to 1/32 of the size,
  369. Firefox smears it sideways, other engines differ again. A fixed
  370. stroke in the PDF was therefore heavier than the screen in one
  371. browser and lighter in the next, so the amount is measured rather
  372. than assumed.
  373. Bold: the run's text is drawn on a canvas in the weight it asked for
  374. (which the browser synthesizes exactly as it does on the page) and in
  375. regular weight filled and stroked with a growing pen - the very thing
  376. the PDF does with text rendering mode 2 - until the two carry the
  377. same ink. Both drawings go through the same rasterizer, so its
  378. anti-aliasing cancels out; in Chrome this lands on Skia's own figure
  379. to the hundredth of a pixel at every size.
  380. Italic: the slant moves every bit of ink sideways in proportion to
  381. its height above the baseline, so the shift of the ink's centre over
  382. the height of that centre is the slant. A browser that does not
  383. synthesize at all measures as no stroke and no slant, and the PDF
  384. follows it. */
  385. var SYNTH_K = 2; // device pixels per css px while measuring
  386. var synthCache = {};
  387. var synthCtx = null;
  388. function synthCanvas(w, h) {
  389. if (!synthCtx) {
  390. var c = document.createElement("canvas");
  391. synthCtx = c.getContext("2d", { willReadFrequently: true });
  392. }
  393. var cv = synthCtx.canvas;
  394. if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; }
  395. return synthCtx;
  396. }
  397. function quoteFamily(f) {
  398. return '"' + String(f).replace(/["\\]/g, "") + '"';
  399. }
  400. // skiaBold is Chrome's own figure, used when nothing can be measured
  401. function skiaBold(size) {
  402. var t = clamp((size - 9) / 27, 0, 1);
  403. return size * (1 / 24 + (1 / 32 - 1 / 24) * t);
  404. }
  405. function inkStats(g, w, h) {
  406. var d = g.getImageData(0, 0, w, h).data;
  407. var sum = 0, sx = 0, sy = 0;
  408. for (var i = 3, p = 0; i < d.length; i += 4, p++) {
  409. var a = d[i];
  410. if (!a) continue;
  411. sum += a;
  412. sx += a * (p % w);
  413. sy += a * ((p / w) | 0);
  414. }
  415. return { ink: sum / 255, cx: sum ? sx / sum : 0, cy: sum ? sy / sum : 0 };
  416. }
  417. /* synthStyle answers how much the browser emboldens (stroke width, css
  418. px) and slants (horizontal shift per unit of height) one face at one
  419. size, for the faces faceFor() says are synthesized. sample is text
  420. set in that face: what is measured is the same glyphs. */
  421. function synthStyle(family, weight, italic, sizePx, sample, wantBold, wantItalic) {
  422. var out = { bold: 0, skew: 0 };
  423. var text = visibleText(String(sample || "")).replace(/\s+/g, "").substring(0, 12);
  424. weight = parseInt(weight, 10) || (wantBold ? 700 : 400);
  425. if (wantBold) out.bold = measureBold(family, weight, italic, sizePx, text);
  426. if (wantItalic) out.skew = measureSkew(family, weight, text);
  427. return out;
  428. }
  429. function measureBold(family, weight, italic, size, text) {
  430. var key = "b|" + family + "|" + weight + "|" + (italic ? 1 : 0) + "|" + size.toFixed(2);
  431. if (synthCache[key] !== undefined) return synthCache[key];
  432. var w = skiaBold(size);
  433. try {
  434. if (!text) throw new Error("nothing to measure");
  435. var style = italic ? "italic " : "";
  436. var fam = quoteFamily(family);
  437. var g = synthCanvas(1, 1);
  438. g.setTransform(1, 0, 0, 1, 0, 0);
  439. g.font = style + "400 " + size + "px " + fam;
  440. var tw = g.measureText(text).width;
  441. var W = Math.ceil((tw + size * 1.5) * SYNTH_K), H = Math.ceil(size * 2.2 * SYNTH_K);
  442. // a measurement this large is not worth its cost; Chrome's
  443. // figure is proportional above 36px, so measure smaller and scale
  444. var scale = 1;
  445. if (W * H > 1600000) {
  446. scale = Math.sqrt(1600000 / (W * H));
  447. W = Math.ceil(W * scale); H = Math.ceil(H * scale);
  448. }
  449. var k = SYNTH_K * scale;
  450. g = synthCanvas(W, H);
  451. var draw = function (wt, pen) {
  452. g.setTransform(1, 0, 0, 1, 0, 0);
  453. g.clearRect(0, 0, W, H);
  454. g.setTransform(k, 0, 0, k, 0, 0);
  455. g.font = style + wt + " " + size + "px " + fam;
  456. g.fillStyle = "#000";
  457. g.textBaseline = "alphabetic";
  458. g.fillText(text, size * 0.5, size * 1.5);
  459. if (pen > 0) {
  460. g.strokeStyle = "#000";
  461. g.lineWidth = pen;
  462. g.lineJoin = "miter";
  463. g.miterLimit = 10;
  464. g.strokeText(text, size * 0.5, size * 1.5);
  465. }
  466. return inkStats(g, W, H).ink;
  467. };
  468. var base = draw(400, 0);
  469. var target = draw(weight, 0);
  470. if (!(base > 0)) throw new Error("no ink");
  471. if (target <= base * 1.002) {
  472. w = 0; // this browser does not embolden
  473. } else {
  474. var lo = 0, hi = size / 6;
  475. for (var it = 0; it < 14; it++) {
  476. var mid = (lo + hi) / 2;
  477. if (draw(400, mid) < target) lo = mid; else hi = mid;
  478. }
  479. w = (lo + hi) / 2;
  480. }
  481. } catch (e) { /* keep Chrome's figure */ }
  482. synthCache[key] = w;
  483. return w;
  484. }
  485. function measureSkew(family, weight, text) {
  486. var key = "i|" + family + "|" + weight;
  487. if (synthCache[key] !== undefined) return synthCache[key];
  488. var s = 0.25; // Chrome's and Safari's slant
  489. try {
  490. if (!text) throw new Error("nothing to measure");
  491. var size = 64;
  492. var fam = quoteFamily(family);
  493. var g = synthCanvas(1, 1);
  494. g.setTransform(1, 0, 0, 1, 0, 0);
  495. g.font = weight + " " + size + "px " + fam;
  496. var W = Math.ceil(g.measureText(text).width + size * 2), H = Math.ceil(size * 2);
  497. var baseY = size * 1.4;
  498. g = synthCanvas(W, H);
  499. var draw = function (style) {
  500. g.setTransform(1, 0, 0, 1, 0, 0);
  501. g.clearRect(0, 0, W, H);
  502. g.font = style + weight + " " + size + "px " + fam;
  503. g.fillStyle = "#000";
  504. g.textBaseline = "alphabetic";
  505. g.fillText(text, size * 0.5, baseY);
  506. return inkStats(g, W, H);
  507. };
  508. var up = draw(""), it = draw("italic ");
  509. var rise = baseY - up.cy;
  510. if (!(up.ink > 0) || !(rise > 1)) throw new Error("no ink");
  511. s = clamp((it.cx - up.cx) / rise, 0, 0.5);
  512. } catch (e) { /* keep the usual slant */ }
  513. synthCache[key] = s;
  514. return s;
  515. }
  516. /* eachTextNode is the walk both the font pre-pass and the run collector
  517. make, kept in one place so they cannot disagree about what counts as
  518. text on the slide. */
  519. function eachTextNode(rootEl, fn) {
  520. var walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT, null);
  521. var node;
  522. while ((node = walker.nextNode())) {
  523. var text = node.nodeValue;
  524. if (!text || !visibleText(text).trim()) continue;
  525. var parent = node.parentElement;
  526. if (!parent) continue;
  527. var cs = window.getComputedStyle(parent);
  528. if (cs.visibility === "hidden" || cs.display === "none") continue;
  529. // source newlines and tabs are whitespace the browser already
  530. // collapsed; they must not reach a font, but the string has to
  531. // keep its length - the line split indexes back into the node
  532. fn(node, text.replace(/[\u0000-\u001F\u007F]/g, " "), cs);
  533. }
  534. }
  535. /* ---------------- small helpers ---------------- */
  536. function px(v) { return v * PX_TO_PT; }
  537. function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  538. /* parseFill returns both halves of a CSS colour: the colour itself and
  539. its alpha. The alpha matters - a table's header band is a translucent
  540. wash of the theme accent, and dropping it turns a tint into a slab. */
  541. function parseFill(css) {
  542. if (!css) return null;
  543. var m = /^rgba?\(([^)]+)\)$/i.exec(String(css).trim());
  544. if (m) {
  545. var p = m[1].split(",").map(function (x) { return parseFloat(x); });
  546. var a = p.length >= 4 ? clamp(p[3], 0, 1) : 1;
  547. if (a === 0) return null;
  548. return {
  549. c: PDFLib.rgb(clamp(p[0] / 255, 0, 1), clamp(p[1] / 255, 0, 1), clamp(p[2] / 255, 0, 1)),
  550. a: a
  551. };
  552. }
  553. var t = String(css).trim();
  554. var h = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(t);
  555. if (!h) return null;
  556. var v = h[1];
  557. var alpha = 1;
  558. if (v.length === 8) { alpha = parseInt(v.substring(6, 8), 16) / 255; v = v.substring(0, 6); }
  559. if (v.length === 3) v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2];
  560. if (alpha === 0) return null;
  561. var n = parseInt(v, 16);
  562. return {
  563. c: PDFLib.rgb(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255),
  564. a: alpha
  565. };
  566. }
  567. // parseColor is parseFill when only the colour is wanted
  568. /* contrastOf answers "what colour shows up on this fill" - only used
  569. for a shape's markings when it has no stroke colour of its own */
  570. function contrastOf(css) {
  571. var f = parseFill(css);
  572. if (!f) return "#333333";
  573. var lum = 0.299 * f.c.red + 0.587 * f.c.green + 0.114 * f.c.blue;
  574. return lum > 0.6 ? "#333333" : "#ffffff";
  575. }
  576. function parseColor(css) {
  577. var f = parseFill(css);
  578. return f ? f.c : null;
  579. }
  580. function dataUrlBytes(src) {
  581. var comma = String(src || "").indexOf(",");
  582. if (comma < 0) return null;
  583. var head = src.substring(0, comma);
  584. if (head.indexOf(";base64") < 0) return null;
  585. var bin = atob(src.substring(comma + 1));
  586. var out = new Uint8Array(bin.length);
  587. for (var i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  588. return { bytes: out, mime: (/^data:([^;]+)/.exec(head) || [])[1] || "" };
  589. }
  590. /* ---------------- the drawing context ---------------- */
  591. /* Page is a thin wrapper that flips the y axis once: the editor's
  592. coordinates run down from the top-left of the slide, a PDF page's run
  593. up from the bottom-left, and mixing the two up is the single easiest
  594. way to get an export subtly wrong. */
  595. function Page(page, pdfDoc, fonts, heightPx) {
  596. this.p = page;
  597. this.doc = pdfDoc;
  598. this.fonts = fonts;
  599. this.h = heightPx;
  600. this.gsCache = {};
  601. }
  602. Page.prototype.y = function (topPx) { return px(this.h - topPx); };
  603. Page.prototype.rect = function (x, y, w, h, opts) {
  604. this.p.drawRectangle({
  605. x: px(x), y: this.y(y + h), width: px(w), height: px(h),
  606. color: opts.fill || undefined,
  607. borderColor: opts.stroke || undefined,
  608. borderWidth: opts.strokeW ? px(opts.strokeW) : undefined,
  609. borderDashArray: opts.dash ? [px(opts.strokeW * 3), px(opts.strokeW * 2)] : undefined,
  610. opacity: opts.fillOpacity !== undefined ? opts.fillOpacity : opts.opacity,
  611. borderOpacity: opts.strokeOpacity !== undefined ? opts.strokeOpacity : opts.opacity
  612. });
  613. };
  614. // ops pushes raw content-stream operators, which is how the clip paths
  615. // and the matrices below are expressed
  616. Page.prototype.ops = function (list) {
  617. this.p.pushOperators.apply(this.p, list);
  618. };
  619. Page.prototype.save = function () { this.ops([PDFLib.pushGraphicsState()]); };
  620. Page.prototype.restore = function () { this.ops([PDFLib.popGraphicsState()]); };
  621. // alpha returns the name of an ExtGState for a given opacity, making one
  622. // only the first time each distinct value is used on this page
  623. Page.prototype.alpha = function (a) {
  624. var key = "a" + Math.round(a * 1000);
  625. if (!this.gsCache[key] && this.p.recordAlpha) {
  626. // a recording page (pdfdraw.js) makes the state where it is replayed
  627. this.p.recordAlpha(key, a);
  628. this.gsCache[key] = true;
  629. } else if (!this.gsCache[key]) {
  630. var ref = this.doc.context.register(this.doc.context.obj({
  631. Type: "ExtGState", ca: a, CA: a
  632. }));
  633. this.p.node.setExtGState(PDFLib.PDFName.of(key), ref);
  634. this.gsCache[key] = true;
  635. }
  636. return key;
  637. };
  638. /* ---------------- text ---------------- */
  639. /* A run is one uniformly formatted fragment of a single line, measured
  640. off the live DOM: its box, its baseline and the style in force. */
  641. function collectRuns(rootEl, origin) {
  642. var runs = [];
  643. eachTextNode(rootEl, function (node, text, cs) {
  644. // one entry per line box the fragment occupies
  645. var range = document.createRange();
  646. range.selectNodeContents(node);
  647. var rects = Array.prototype.slice.call(range.getClientRects());
  648. if (!rects.length) return;
  649. var ls = parseFloat(cs.letterSpacing);
  650. var shared = {
  651. origin: origin,
  652. node: node,
  653. names: familyList(cs.fontFamily),
  654. family: cs.fontFamily,
  655. metrics: fontMetricsOf(cs),
  656. size: parseFloat(cs.fontSize) || 12,
  657. weight: parseInt(cs.fontWeight, 10) || (cs.fontWeight === "bold" ? 700 : 400),
  658. italic: cs.fontStyle === "italic" || cs.fontStyle === "oblique",
  659. underline: cs.textDecorationLine.indexOf("underline") >= 0,
  660. strike: cs.textDecorationLine.indexOf("line-through") >= 0,
  661. transform: cs.textTransform,
  662. letterSpacing: isFinite(ls) ? ls : 0,
  663. color: cs.color,
  664. background: cs.backgroundColor
  665. };
  666. splitByLine(node, text, rects).forEach(function (ln) {
  667. var r = Object.create(shared);
  668. r.text = ln.text;
  669. r.rect = ln.rect;
  670. r.start = ln.start;
  671. runs.push(r);
  672. });
  673. });
  674. return runs;
  675. }
  676. /* splitByLine maps a text node's client rects back onto the substrings
  677. that produced them, so each line can be drawn at its own baseline.
  678. Character-by-character is the only reliable way: the browser decides
  679. where the break went, and only it knows. start is where each line's
  680. text begins in the node. */
  681. function splitByLine(node, text, lineRects) {
  682. if (lineRects.length === 1) {
  683. return [{ text: text, rect: lineRects[0], start: 0 }];
  684. }
  685. var range = document.createRange();
  686. var out = [];
  687. var cur = "";
  688. var curStart = 0;
  689. var curTop = null;
  690. var curRect = null;
  691. for (var i = 0; i < text.length; i++) {
  692. range.setStart(node, i);
  693. range.setEnd(node, i + 1);
  694. var r = range.getBoundingClientRect();
  695. if (r.width === 0 && r.height === 0) { cur += text[i]; continue; }
  696. var top = Math.round(r.top * 10) / 10;
  697. if (curTop === null || Math.abs(top - curTop) < 0.6) {
  698. if (curTop === null) { curTop = top; curRect = { left: r.left, top: r.top, bottom: r.bottom, right: r.right }; }
  699. else { curRect.right = Math.max(curRect.right, r.right); }
  700. cur += text[i];
  701. } else {
  702. out.push({ text: cur, rect: curRect, start: curStart });
  703. cur = text[i];
  704. curStart = i;
  705. curTop = top;
  706. curRect = { left: r.left, top: r.top, bottom: r.bottom, right: r.right };
  707. }
  708. }
  709. if (cur !== "" && curRect) out.push({ text: cur, rect: curRect, start: curStart });
  710. return out.length ? out : [{ text: text, rect: lineRects[0], start: 0 }];
  711. }
  712. // canDrawAsText is the whole fallback decision, in one place: every
  713. // character of every run has to have a font that can show it
  714. function canDrawAsText(runs, fonts) {
  715. for (var i = 0; i < runs.length; i++) {
  716. var r = runs[i];
  717. if (!segmentText(transformText(r.text, r.transform), r.names, fonts, r.weight >= 600, r.italic)) return false;
  718. }
  719. return true;
  720. }
  721. /* planRun decides how one line fragment is drawn: as text wherever a
  722. font can show it, and as a picture only for the characters nothing
  723. can (an emoji, a script the suite does not ship). Each piece comes
  724. back with its place in the line, measured off the DOM, so a picture
  725. in the middle of a sentence lands between the words around it:
  726. [{ text, dx, w, fit, raster }]
  727. dx is from the run's left edge; fit is the width to fit text to, 0
  728. when the piece begins or ends on a space (the browser collapses
  729. those, the measurement does not). */
  730. function planRun(r, fonts) {
  731. var bold = r.weight >= 600;
  732. var whole = r.rect.right - r.rect.left;
  733. var shown = transformText(r.text, r.transform);
  734. if (segmentText(shown, r.names, fonts, bold, r.italic)) {
  735. return [{ text: shown, dx: 0, w: whole, fit: /^\s|\s$/.test(r.text) ? 0 : whole, raster: false }];
  736. }
  737. // cut where drawability changes; a character that draws nothing
  738. // (a joiner inside an emoji) stays with what is around it
  739. var parts = [], cur = null;
  740. for (var i = 0; i < r.text.length;) {
  741. var cp = r.text.codePointAt(i), n = cp > 0xFFFF ? 2 : 1;
  742. var ok;
  743. if (ignorableCp(cp)) {
  744. ok = cur ? cur.ok : true;
  745. } else {
  746. var shownCp = transformText(String.fromCodePoint(cp), r.transform).codePointAt(0);
  747. var res = resolveChar(shownCp, r.names, fonts, bold, r.italic);
  748. ok = !!(res && !res.need);
  749. }
  750. if (cur && cur.ok === ok) cur.end = i + n;
  751. else { cur = { ok: ok, start: i, end: i + n }; parts.push(cur); }
  752. i += n;
  753. }
  754. /* Each piece is measured against the whole line at the same moment,
  755. so the place comes out right even when the page is no longer at
  756. the zoom it was collected at (Docs hands the page back before it
  757. writes its display list). A line whose text node has changed
  758. since cannot be measured: it goes in whole, as a picture. */
  759. var range = document.createRange();
  760. var now = null;
  761. try {
  762. range.setStart(r.node, r.start);
  763. range.setEnd(r.node, r.start + r.text.length);
  764. now = range.getBoundingClientRect();
  765. } catch (e) { now = null; }
  766. if (!now || !(now.width > 0) || r.node.nodeValue.substr(r.start, r.text.length) !== r.text) {
  767. return [{ text: shown, dx: 0, w: whole, fit: 0, raster: true }];
  768. }
  769. var k = whole / now.width;
  770. var out = [];
  771. parts.forEach(function (p) {
  772. var text = r.text.substring(p.start, p.end);
  773. range.setStart(r.node, r.start + p.start);
  774. range.setEnd(r.node, r.start + p.end);
  775. var b = range.getBoundingClientRect();
  776. var dx = (b.left - now.left) * k, w = b.width * k;
  777. if (!(w > 0) && !visibleText(text).trim()) return;
  778. out.push({
  779. text: transformText(text, r.transform), dx: dx, w: w,
  780. fit: /^\s|\s$/.test(text) ? 0 : w, raster: !p.ok
  781. });
  782. });
  783. return out;
  784. }
  785. /* rasterText draws a piece of a run as a picture of itself, through
  786. the page's own canvas - which has the page's fonts, so the picture is
  787. set in the face the screen used, unlike an SVG image of the element.
  788. Returns { src, x, y, w, h } relative to the piece's left edge and the
  789. line's top, or null. */
  790. function rasterText(r, text, w, lineH, baseline) {
  791. try {
  792. var pad = Math.ceil(r.size * 0.3);
  793. var cw = Math.max(1, w) + pad * 2, ch = lineH + pad * 2;
  794. var k = RASTER_SCALE;
  795. var c = document.createElement("canvas");
  796. c.width = Math.ceil(cw * k);
  797. c.height = Math.ceil(ch * k);
  798. var g = c.getContext("2d");
  799. g.scale(k, k);
  800. g.font = (r.italic ? "italic " : "") + r.weight + " " + r.size + "px " + r.family;
  801. if (r.letterSpacing && "letterSpacing" in g) g.letterSpacing = r.letterSpacing + "px";
  802. g.fillStyle = r.color;
  803. g.textBaseline = "alphabetic";
  804. g.fillText(text, pad, pad + baseline);
  805. return { src: c.toDataURL("image/png"), x: -pad, y: -pad, w: cw, h: ch };
  806. } catch (e) {
  807. return null;
  808. }
  809. }
  810. /* drawFragment puts one line fragment on the page as real text, in as
  811. many pieces as it takes fonts to spell it.
  812. fitPx, when it is given, is the width the browser gave the fragment.
  813. The text is squeezed or stretched to exactly that with Tz, which
  814. costs nothing when the PDF font is the one the browser used (the
  815. ratio is 1) and is what keeps a substituted face - a system font we
  816. could not embed - from pushing the rest of the line out of place.
  817. Bold that a shipped face does not have is stroked rather than filled,
  818. and italic it does not have is slanted, both by as much as the
  819. browser does it (synthStyle). Neither moves the advance widths, so
  820. the two stay in step.
  821. letterSpacing (css px) goes in as character spacing, which is how
  822. CSS adds it too: after every character, the last one included. */
  823. function drawFragment(pg, spec) {
  824. var fonts = pg.fonts;
  825. var segs = segmentText(spec.text, spec.names, fonts, spec.bold, spec.italic);
  826. if (!segs || !segs.length) return 0;
  827. var ls = spec.letterSpacing || 0;
  828. var total = 0;
  829. for (var i = 0; i < segs.length; i++) {
  830. var face = faceOf(segs[i].res, fonts, spec.bold, spec.italic);
  831. if (!face) return 0;
  832. segs[i].face = face;
  833. segs[i].w = face.font.widthOfTextAtSize(segs[i].text, spec.sizePx) +
  834. (ls ? ls * Array.from(segs[i].text).length : 0);
  835. if (face.synthBold || face.synthItalic) {
  836. segs[i].synth = synthStyle(face.family, spec.weight || (spec.bold ? 700 : 400), spec.italic,
  837. spec.sizePx, segs[i].text, face.synthBold, face.synthItalic);
  838. }
  839. total += segs[i].w;
  840. }
  841. var scale = 1;
  842. if (spec.fitPx > 0 && total > 0) {
  843. var ratio = spec.fitPx / total;
  844. // a ratio far from 1 means the measurement, not the font, is
  845. // wrong (a collapsed space, a transform) - leave it alone
  846. if (ratio > 0.5 && ratio < 2 && Math.abs(ratio - 1) > 0.005) scale = ratio;
  847. }
  848. var col = spec.color || PDFLib.rgb(0, 0, 0);
  849. var cursor = spec.xPx;
  850. segs.forEach(function (seg) {
  851. pg.save();
  852. var ops = [];
  853. var synth = seg.synth || { bold: 0, skew: 0 };
  854. if (scale !== 1) ops.push(PDFLib.setCharacterSqueeze(scale * 100));
  855. if (ls) ops.push(PDFLib.setCharacterSpacing(px(ls)));
  856. if (synth.bold > 0.01) {
  857. ops.push(PDFLib.setTextRenderingMode(PDFLib.TextRenderingMode.FillAndOutline));
  858. ops.push(PDFLib.setLineWidth(px(synth.bold)));
  859. ops.push(PDFLib.setStrokingColor(col));
  860. }
  861. if (ops.length) pg.ops(ops);
  862. var opt = {
  863. x: px(cursor), y: pg.y(spec.baselinePx),
  864. size: px(spec.sizePx), font: seg.face.font, color: col
  865. };
  866. // the text matrix's horizontal shear by height (pdf-lib calls
  867. // that angle ySkew)
  868. if (synth.skew > 0.001) opt.ySkew = PDFLib.radians(Math.atan(synth.skew));
  869. pg.p.drawText(seg.text, opt);
  870. pg.restore();
  871. cursor += seg.w * scale;
  872. });
  873. return total * scale;
  874. }
  875. /* drawRuns puts every run on the page at the baseline the browser laid
  876. it out on, in the width the browser gave it. A character no font can
  877. show comes in as a picture of itself, in its own place in the line
  878. (planRun); drawImage(src, x, y, w, h) puts that picture on the page,
  879. and without it such characters are left out. */
  880. function drawRuns(pg, runs, drawImage) {
  881. runs.forEach(function (r) {
  882. var x = r.origin.x + (r.rect.left - r.origin.left);
  883. var top = r.origin.y + (r.rect.top - r.origin.top);
  884. var lineH = r.rect.bottom - r.rect.top;
  885. var domW = r.rect.right - r.rect.left;
  886. var glyphH = r.metrics.ascent + r.metrics.descent;
  887. var baseline = (lineH - glyphH) / 2 + r.metrics.ascent;
  888. var col = parseColor(r.color) || PDFLib.rgb(0, 0, 0);
  889. var bg = parseFill(r.background);
  890. if (bg) pg.rect(x, top, domW, lineH, { fill: bg.c, fillOpacity: bg.a });
  891. var drawn = 0;
  892. planRun(r, pg.fonts).forEach(function (pc) {
  893. if (pc.raster) {
  894. var pic = drawImage ? rasterText(r, pc.text, pc.w, lineH, baseline) : null;
  895. if (pic) drawImage(pic.src, x + pc.dx + pic.x, top + pic.y, pic.w, pic.h);
  896. drawn += pc.w;
  897. return;
  898. }
  899. drawn += drawFragment(pg, {
  900. text: pc.text, names: r.names,
  901. bold: r.weight >= 600, weight: r.weight, italic: r.italic,
  902. sizePx: r.size, xPx: x + pc.dx, baselinePx: top + baseline,
  903. color: col, fitPx: pc.fit, letterSpacing: r.letterSpacing
  904. });
  905. });
  906. if (r.underline || r.strike) {
  907. var yOff = r.underline ? baseline + r.size * 0.11 : baseline - r.size * 0.28;
  908. pg.rect(x, top + yOff, drawn || domW, Math.max(0.7, r.size * 0.06), { fill: col });
  909. }
  910. });
  911. }
  912. /* ---------------- rasterizing one element ---------------- */
  913. /* The documented last resort, and it has one rule: the pixels come out
  914. of a render of the whole slide, then get cropped to the element that
  915. needed them.
  916. Rasterizing an element on its own looks tempting and is wrong.
  917. html2canvas re-renders a *clone*, and a clone torn out of its
  918. absolutely positioned parent loses the width it was laid out in -
  919. so the text re-wraps and the export stops matching the editor,
  920. which is the one thing it must not do. Rendering the slide keeps
  921. every element in the context it was measured in.
  922. What lands in the PDF is still only that element's own box: one
  923. image, at its own position, with everything else on the page a real
  924. PDF object. */
  925. /* The picture is taken through an SVG <foreignObject>, which means the
  926. *browser* lays the element out and paints it - the same engine, the
  927. same fonts, the same line breaks as the editor.
  928. This is not the obvious choice, so: html2canvas was tried first and
  929. is wrong for this. It re-implements layout over a clone, and on
  930. mixed CJK/Latin text with pre-wrap it breaks lines somewhere else
  931. than the browser did, which is precisely the failure this whole
  932. rework exists to remove. A foreignObject cannot re-wrap anything,
  933. because it is not re-laying anything out.
  934. The cost is that computed styles have to be inlined onto the clone
  935. (an SVG image cannot reach the page's stylesheets) and every
  936. resource must already be a data URL - which, in a slide, it is. */
  937. /* ext, when given, is the area to capture relative to the element's own
  938. box ({ x, y, w, h }, x and y usually <= 0): what overflows the box -
  939. a table whose rows grew past its frame, text running out of its box -
  940. is on the slide, so it belongs in the picture too. */
  941. function rasterizeElement(el, w, h, ext) {
  942. try {
  943. var clone = el.cloneNode(true);
  944. inlineStyles(el, clone);
  945. ext = ext || { x: 0, y: 0, w: w, h: h };
  946. // the foreignObject supplies the box, so the clone must not
  947. // carry the absolute placement it had on the slide
  948. clone.style.position = "absolute";
  949. clone.style.left = (-ext.x) + "px";
  950. clone.style.top = (-ext.y) + "px";
  951. clone.style.transform = "none";
  952. clone.style.margin = "0";
  953. clone.style.width = w + "px";
  954. clone.style.height = h + "px";
  955. var cw = Math.max(1, Math.ceil(ext.w)), ch = Math.max(1, Math.ceil(ext.h));
  956. /* The markup inside a foreignObject has to be well-formed XML,
  957. and innerHTML is not: it writes <br> unclosed, which makes the
  958. whole SVG fail to parse and the element vanish from the page.
  959. XMLSerializer writes real XHTML, so it cannot. */
  960. var wrap = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
  961. wrap.setAttribute("style", "position:relative;overflow:hidden;width:" + cw + "px;height:" + ch + "px;");
  962. wrap.appendChild(clone);
  963. var xhtml = new XMLSerializer().serializeToString(wrap);
  964. var svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + cw +
  965. '" height="' + ch + '"><foreignObject x="0" y="0" width="' + cw +
  966. '" height="' + ch + '">' + xhtml + "</foreignObject></svg>";
  967. var url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
  968. return new Promise(function (resolve) {
  969. var img = new Image();
  970. img.onload = function () {
  971. try {
  972. var c = document.createElement("canvas");
  973. c.width = Math.max(1, Math.round(cw * RASTER_SCALE));
  974. c.height = Math.max(1, Math.round(ch * RASTER_SCALE));
  975. var g = c.getContext("2d");
  976. g.drawImage(img, 0, 0, c.width, c.height);
  977. resolve(c.toDataURL("image/png"));
  978. } catch (e) { resolve(null); }
  979. };
  980. img.onerror = function () { resolve(null); };
  981. img.src = url;
  982. });
  983. } catch (e) {
  984. return Promise.resolve(null);
  985. }
  986. }
  987. /* rasterizeFallback is the very last resort, for the case where even
  988. the foreignObject route fails. html2canvas re-implements layout and
  989. can break mixed-script lines somewhere the browser did not, so it is
  990. only ever reached when the alternative is dropping the element
  991. from the page entirely - which would be worse. */
  992. function rasterizeFallback(el, w, h) {
  993. if (typeof html2canvas === "undefined") return Promise.resolve(null);
  994. return html2canvas(el, {
  995. scale: RASTER_SCALE, useCORS: true, backgroundColor: null, logging: false
  996. }).then(function (canvas) {
  997. return canvas.toDataURL("image/png");
  998. }).catch(function () { return null; });
  999. }
  1000. /* inlineStyles copies the computed style of every node in the subtree
  1001. onto the clone, because the SVG image has no access to the page's
  1002. stylesheets. Slide markup is small - a few divs and spans - so
  1003. walking the whole property list is affordable and leaves nothing out. */
  1004. function inlineStyles(src, dst) {
  1005. var cs = window.getComputedStyle(src);
  1006. var out = "";
  1007. for (var i = 0; i < cs.length; i++) {
  1008. var prop = cs[i];
  1009. out += prop + ":" + cs.getPropertyValue(prop) + ";";
  1010. }
  1011. dst.setAttribute("style", out);
  1012. var a = src.children, b = dst.children;
  1013. for (var j = 0; j < a.length && j < b.length; j++) inlineStyles(a[j], b[j]);
  1014. }
  1015. /* ---------------- images ---------------- */
  1016. /* filteredImageData re-encodes a picture through a canvas when it
  1017. carries a colour treatment. Applying a filter is a pixel operation in
  1018. any renderer, so this is the correct way to do it, not a fallback. */
  1019. function filteredImageData(src, filter, naturalW, naturalH) {
  1020. return new Promise(function (resolve) {
  1021. var img = new Image();
  1022. img.onload = function () {
  1023. try {
  1024. var c = document.createElement("canvas");
  1025. c.width = naturalW || img.naturalWidth;
  1026. c.height = naturalH || img.naturalHeight;
  1027. var g = c.getContext("2d");
  1028. g.filter = filter;
  1029. g.drawImage(img, 0, 0, c.width, c.height);
  1030. resolve(c.toDataURL("image/png"));
  1031. } catch (e) { resolve(null); }
  1032. };
  1033. img.onerror = function () { resolve(null); };
  1034. img.src = src;
  1035. });
  1036. }
  1037. /* sniffImage decides which embedder to use from the bytes themselves
  1038. rather than from a mime string, which a fetched file may not carry */
  1039. function sniffImage(bytes) {
  1040. if (bytes.length > 3 && bytes[0] === 0xFF && bytes[1] === 0xD8) return "jpg";
  1041. return "png";
  1042. }
  1043. /* embedImage caches by source string: a deck that uses one picture on
  1044. twenty slides embeds its bytes once.
  1045. A deck keeps its large media out of the body as media?file= links,
  1046. so a source that is not a data URL is fetched. Embedding the
  1047. original bytes is the point - re-encoding through a canvas would
  1048. turn a photo into a much larger lossless PNG. */
  1049. function makeImageEmbedder(pdfDoc) {
  1050. var cache = {};
  1051. function embedBytes(bytes) {
  1052. return sniffImage(bytes) === "jpg" ? pdfDoc.embedJpg(bytes) : pdfDoc.embedPng(bytes);
  1053. }
  1054. return function (src) {
  1055. if (!src) return Promise.resolve(null);
  1056. if (cache[src]) return cache[src];
  1057. var p;
  1058. var d = dataUrlBytes(src);
  1059. if (d) {
  1060. p = /jpe?g/i.test(d.mime) ? pdfDoc.embedJpg(d.bytes) : embedBytes(d.bytes);
  1061. } else {
  1062. p = fetch(src).then(function (r) {
  1063. if (!r.ok) throw new Error("cannot read " + src);
  1064. return r.arrayBuffer();
  1065. }).then(function (buf) {
  1066. return embedBytes(new Uint8Array(buf));
  1067. });
  1068. }
  1069. cache[src] = p.catch(function () { return null; });
  1070. return cache[src];
  1071. };
  1072. }
  1073. return {
  1074. PX_TO_PT: PX_TO_PT,
  1075. RASTER_SCALE: RASTER_SCALE,
  1076. STD_FAMILIES: STD_FAMILIES,
  1077. winAnsiCp: winAnsiCp,
  1078. ignorableCp: ignorableCp,
  1079. visibleText: visibleText,
  1080. transformText: transformText,
  1081. synthStyle: synthStyle,
  1082. haveFamily: haveFamily,
  1083. familyList: familyList,
  1084. loadFontkit: loadFontkit,
  1085. makeFonts: makeFonts,
  1086. resolveChar: resolveChar,
  1087. segmentText: segmentText,
  1088. faceOf: faceOf,
  1089. fontMetricsOf: fontMetricsOf,
  1090. eachTextNode: eachTextNode,
  1091. px: px,
  1092. clamp: clamp,
  1093. parseFill: parseFill,
  1094. parseColor: parseColor,
  1095. contrastOf: contrastOf,
  1096. dataUrlBytes: dataUrlBytes,
  1097. Page: Page,
  1098. collectRuns: collectRuns,
  1099. splitByLine: splitByLine,
  1100. canDrawAsText: canDrawAsText,
  1101. planRun: planRun,
  1102. rasterText: rasterText,
  1103. drawFragment: drawFragment,
  1104. drawRuns: drawRuns,
  1105. rasterizeElement: rasterizeElement,
  1106. rasterizeFallback: rasterizeFallback,
  1107. inlineStyles: inlineStyles,
  1108. filteredImageData: filteredImageData,
  1109. sniffImage: sniffImage,
  1110. makeImageEmbedder: makeImageEmbedder
  1111. };
  1112. })();