@richscripts/richtexteditor 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migration/README.md +100 -0
- package/migration/cli.js +294 -0
- package/migration/index.d.ts +18 -0
- package/migration/index.js +242 -0
- package/migration/report-schema-v2.json +127 -0
- package/package.json +11 -1
- package/richtexteditor/CHANGELOG.md +326 -1
- package/richtexteditor/all_plugins.js +14959 -4832
- package/richtexteditor/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/accessibilitychecker.js +8 -7
- package/richtexteditor/plugins/aitoolkit.css +17 -8
- package/richtexteditor/plugins/aitoolkit.js +8 -8
- package/richtexteditor/plugins/all_plugins.js +14959 -4832
- package/richtexteditor/plugins/all_plugins.min.js +62 -42
- package/richtexteditor/plugins/autocorrect.js +353 -0
- package/richtexteditor/plugins/bookmarkcard.js +12 -1
- package/richtexteditor/plugins/changecase.js +238 -0
- package/richtexteditor/plugins/comments.js +15 -28
- package/richtexteditor/plugins/contentminimap.js +6 -5
- package/richtexteditor/plugins/crossreference.js +408 -0
- package/richtexteditor/plugins/documentimport.js +915 -21
- package/richtexteditor/plugins/documentoutline.js +5 -4
- package/richtexteditor/plugins/docxexport.js +960 -0
- package/richtexteditor/plugins/draghandle.js +309 -0
- package/richtexteditor/plugins/footnotes.js +398 -0
- package/richtexteditor/plugins/formatpainter.js +298 -0
- package/richtexteditor/plugins/formattingmarks.js +246 -0
- package/richtexteditor/plugins/html2pdf.js +108 -4
- package/richtexteditor/plugins/imageeditor.js +37 -7
- package/richtexteditor/plugins/insertgallery.js +553 -170
- package/richtexteditor/plugins/keyboarda11y.js +316 -0
- package/richtexteditor/plugins/linenumbers.js +352 -0
- package/richtexteditor/plugins/linkchecker.js +396 -0
- package/richtexteditor/plugins/mentionplugin.js +4 -3
- package/richtexteditor/plugins/mergefields.js +328 -0
- package/richtexteditor/plugins/multilevellist.js +137 -0
- package/richtexteditor/plugins/pagination.js +505 -0
- package/richtexteditor/plugins/pdfexport.js +1137 -0
- package/richtexteditor/plugins/permanentpen.js +211 -0
- package/richtexteditor/plugins/revisionhistory.js +47 -34
- package/richtexteditor/plugins/rtlui.js +150 -0
- package/richtexteditor/plugins/sanitizer.js +486 -0
- package/richtexteditor/plugins/slashcommand.js +142 -17
- package/richtexteditor/plugins/spellcheck.js +6 -3
- package/richtexteditor/plugins/tableofcontents.js +353 -0
- package/richtexteditor/plugins/tabletools.js +342 -0
- package/richtexteditor/plugins/textdirection.js +313 -0
- package/richtexteditor/plugins/trackedchanges.js +37 -6
- package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
- package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
- package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
- package/richtexteditor/plugins/watermark.js +214 -0
- package/richtexteditor/plugins/wordexport.js +0 -0
- package/richtexteditor/plugins/yjscollab.js +49 -11
- package/richtexteditor/rte-config.js +44 -44
- package/richtexteditor/rte.js +91 -63
- package/richtexteditor/rte_theme_default.css +462 -384
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-08-02 PDF export that produces REAL TEXT.
|
|
4
|
+
//
|
|
5
|
+
// The existing html2pdf path renders the document with html2canvas and drops a
|
|
6
|
+
// JPEG onto each page. It is pixel-accurate, which is genuinely the right answer
|
|
7
|
+
// when the layout is the point — but the resulting PDF is a picture:
|
|
8
|
+
//
|
|
9
|
+
// - nothing is selectable, copyable or searchable
|
|
10
|
+
// - screen readers find an empty document (a hard accessibility failure, and
|
|
11
|
+
// an outright blocker for anyone shipping into government or education)
|
|
12
|
+
// - links are painted, not clickable
|
|
13
|
+
// - a text-only page costs hundreds of KB instead of a few
|
|
14
|
+
// - zooming or printing shows resampling artefacts
|
|
15
|
+
//
|
|
16
|
+
// This writes the PDF file format directly: no library, no canvas, no network.
|
|
17
|
+
// Both exporters stay available and neither changes the other's behaviour —
|
|
18
|
+
// `exportpdf` is vector, `html2pdf` remains raster.
|
|
19
|
+
//
|
|
20
|
+
// Why writing PDF by hand is reasonable here: a text PDF is a small object graph
|
|
21
|
+
// plus a content stream, and the 14 standard fonts need no embedding at all. The
|
|
22
|
+
// hard part of a PDF generator is font subsetting, and using the base-14 fonts
|
|
23
|
+
// removes it entirely.
|
|
24
|
+
//
|
|
25
|
+
// API:
|
|
26
|
+
// editor.getPdfBytes(options) -> Uint8Array
|
|
27
|
+
// editor.exportToPdf(filename, options)-> triggers a download
|
|
28
|
+
// Command: exec_command "exportpdf".
|
|
29
|
+
// Config:
|
|
30
|
+
// config.pdfExport = false // disable
|
|
31
|
+
// config.pdfExportFileName = "report"
|
|
32
|
+
// config.pdfExportFont = "helvetica" // helvetica | times | courier
|
|
33
|
+
// config.pdfExportBaseFontSize = 11 // points
|
|
34
|
+
// config.pdfExportTitle / pdfExportAuthor // document metadata
|
|
35
|
+
RTE_DefaultConfig.plugin_pdfexport = RTE_Plugin_PdfExport;
|
|
36
|
+
if (typeof RTE_DefaultConfig.pdfExport === "undefined") RTE_DefaultConfig.pdfExport = true;
|
|
37
|
+
|
|
38
|
+
function RTE_Plugin_PdfExport() {
|
|
39
|
+
var obj = this;
|
|
40
|
+
var config, editor;
|
|
41
|
+
|
|
42
|
+
obj.PluginName = "PdfExport";
|
|
43
|
+
|
|
44
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
45
|
+
|
|
46
|
+
obj.InitEditor = function (argeditor) {
|
|
47
|
+
editor = argeditor;
|
|
48
|
+
if (config.pdfExport === false) return;
|
|
49
|
+
|
|
50
|
+
editor.getPdfBytes = function (options) { return buildPdf(options || {}); };
|
|
51
|
+
editor.exportToPdf = function (filename, options) {
|
|
52
|
+
var bytes = buildPdf(options || {});
|
|
53
|
+
var base = sanitizeName(filename) || sanitizeName(config.pdfExportFileName) || defaultBase();
|
|
54
|
+
return download(bytes, base + ".pdf");
|
|
55
|
+
};
|
|
56
|
+
editor.downloadPdf = editor.exportToPdf;
|
|
57
|
+
|
|
58
|
+
editor.attachEvent("exec_command_exportpdf", function (state) {
|
|
59
|
+
state.returnValue = true;
|
|
60
|
+
state.stopBubble = true;
|
|
61
|
+
try { editor.exportToPdf(); }
|
|
62
|
+
catch (e) { if (window.console) console.error("pdfexport:", e); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The slash-menu entry lives in slashcommand.js, gated on
|
|
66
|
+
// `typeof editor.exportToPdf === "function"`. Registering from here
|
|
67
|
+
// instead would silently do nothing: plugins initialise in bundle
|
|
68
|
+
// order, "pdfexport" sorts before "slashcommand", and
|
|
69
|
+
// editor.slashCommands does not exist yet at this point.
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------- units
|
|
73
|
+
var PT_PER_IN = 72;
|
|
74
|
+
var PAGE_SIZES = { // points, portrait
|
|
75
|
+
letter: [612, 792], legal: [612, 1008], tabloid: [792, 1224],
|
|
76
|
+
a3: [842, 1191], a4: [595, 842], a5: [420, 595]
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function toPoints(v, fallback) {
|
|
80
|
+
if (typeof v === "number" && isFinite(v)) return v * PT_PER_IN; // bare number = inches
|
|
81
|
+
if (typeof v !== "string") return fallback;
|
|
82
|
+
var m = /^\s*(-?[\d.]+)\s*(mm|cm|in|px|pt)?\s*$/.exec(v);
|
|
83
|
+
if (!m) return fallback;
|
|
84
|
+
var n = parseFloat(m[1]);
|
|
85
|
+
if (!isFinite(n)) return fallback;
|
|
86
|
+
switch (m[2]) {
|
|
87
|
+
case "mm": return n * PT_PER_IN / 25.4;
|
|
88
|
+
case "cm": return n * PT_PER_IN / 2.54;
|
|
89
|
+
case "pt": return n;
|
|
90
|
+
case "px": return n * 0.75; // CSS px at 96dpi
|
|
91
|
+
default: return n * PT_PER_IN;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pageGeometry(options) {
|
|
96
|
+
var setup = null;
|
|
97
|
+
try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); }
|
|
98
|
+
catch (e) { setup = null; }
|
|
99
|
+
setup = setup || {};
|
|
100
|
+
var fmt = String(options.format || setup.format || "letter").toLowerCase();
|
|
101
|
+
var size = PAGE_SIZES[fmt] || PAGE_SIZES.letter;
|
|
102
|
+
var landscape = String(options.orientation || setup.orientation || "").toLowerCase() === "landscape";
|
|
103
|
+
var w = landscape ? size[1] : size[0];
|
|
104
|
+
var h = landscape ? size[0] : size[1];
|
|
105
|
+
var m = options.margins || setup.margins || {};
|
|
106
|
+
var half = PT_PER_IN / 2;
|
|
107
|
+
return {
|
|
108
|
+
width: w, height: h,
|
|
109
|
+
margin: {
|
|
110
|
+
top: toPoints(m.top, half), right: toPoints(m.right, half),
|
|
111
|
+
bottom: toPoints(m.bottom, half), left: toPoints(m.left, half)
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------- fonts
|
|
117
|
+
//
|
|
118
|
+
// The base-14 fonts need no embedding, but a viewer would then lay text out
|
|
119
|
+
// with ITS metrics while this code wrapped lines using its own. The two
|
|
120
|
+
// disagree and text overruns the margin. Fixing it by shipping AFM width
|
|
121
|
+
// tables would add ~1500 lines of data; instead the widths are measured here
|
|
122
|
+
// and written into the font object as /Widths, which a viewer is required to
|
|
123
|
+
// honour. Layout and rendering then use the same numbers by construction.
|
|
124
|
+
//
|
|
125
|
+
// Measurement is accurate because the base-14 fonts are metric-compatible
|
|
126
|
+
// with fonts every browser has: Helvetica/Arial, Times/Times New Roman,
|
|
127
|
+
// Courier/Courier New were designed to share advance widths.
|
|
128
|
+
var FONT_FAMILIES = {
|
|
129
|
+
helvetica: { css: "Arial, Helvetica, sans-serif", faces: ["Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique"] },
|
|
130
|
+
times: { css: '"Times New Roman", Times, serif', faces: ["Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic"] },
|
|
131
|
+
courier: { css: '"Courier New", Courier, monospace', faces: ["Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"] }
|
|
132
|
+
};
|
|
133
|
+
var FIRST_CHAR = 32, LAST_CHAR = 255;
|
|
134
|
+
|
|
135
|
+
var widthCache = {};
|
|
136
|
+
function widthsFor(familyKey, styleIndex) {
|
|
137
|
+
var key = familyKey + ":" + styleIndex;
|
|
138
|
+
if (widthCache[key]) return widthCache[key];
|
|
139
|
+
var fam = FONT_FAMILIES[familyKey] || FONT_FAMILIES.helvetica;
|
|
140
|
+
var canvas = widthsFor.canvas || (widthsFor.canvas = document.createElement("canvas"));
|
|
141
|
+
var g = canvas.getContext("2d");
|
|
142
|
+
// 1000 units per em is the PDF glyph space; measuring at 100px and
|
|
143
|
+
// scaling by 10 keeps sub-unit rounding away from the result.
|
|
144
|
+
var weight = (styleIndex & 1) ? "bold " : "";
|
|
145
|
+
var slant = (styleIndex & 2) ? "italic " : "";
|
|
146
|
+
g.font = weight + slant + "100px " + fam.css;
|
|
147
|
+
var out = [];
|
|
148
|
+
for (var c = FIRST_CHAR; c <= LAST_CHAR; c++) {
|
|
149
|
+
var w = g.measureText(String.fromCharCode(c)).width;
|
|
150
|
+
out.push(Math.round(w * 10));
|
|
151
|
+
}
|
|
152
|
+
widthCache[key] = out;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function charWidth(familyKey, styleIndex, code) {
|
|
157
|
+
if (code < FIRST_CHAR || code > LAST_CHAR) code = 63; // '?'
|
|
158
|
+
return widthsFor(familyKey, styleIndex)[code - FIRST_CHAR] || 500;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function textWidth(str, familyKey, styleIndex, size) {
|
|
162
|
+
var total = 0;
|
|
163
|
+
for (var i = 0; i < str.length; i++) total += charWidth(familyKey, styleIndex, str.charCodeAt(i));
|
|
164
|
+
return total * size / 1000;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ------------------------------------------------------------ PDF string
|
|
168
|
+
//
|
|
169
|
+
// WinAnsiEncoding covers Latin-1 plus the printable range of CP1252. A
|
|
170
|
+
// character outside it has no glyph in a base-14 font, so it is transliterated
|
|
171
|
+
// rather than written as a byte the viewer would render as garbage.
|
|
172
|
+
var WINANSI_EXTRA = {
|
|
173
|
+
0x20AC: 128, 0x201A: 130, 0x0192: 131, 0x201E: 132, 0x2026: 133, 0x2020: 134,
|
|
174
|
+
0x2021: 135, 0x02C6: 136, 0x2030: 137, 0x0160: 138, 0x2039: 139, 0x0152: 140,
|
|
175
|
+
0x017D: 142, 0x2018: 145, 0x2019: 146, 0x201C: 147, 0x201D: 148, 0x2022: 149,
|
|
176
|
+
0x2013: 150, 0x2014: 151, 0x02DC: 152, 0x2122: 153, 0x0161: 154, 0x203A: 155,
|
|
177
|
+
0x0153: 156, 0x017E: 158, 0x0178: 159
|
|
178
|
+
};
|
|
179
|
+
function toWinAnsi(str) {
|
|
180
|
+
var out = "";
|
|
181
|
+
for (var i = 0; i < str.length; i++) {
|
|
182
|
+
var cp = str.charCodeAt(i);
|
|
183
|
+
if (cp < 256) { out += String.fromCharCode(cp); continue; }
|
|
184
|
+
var mapped = WINANSI_EXTRA[cp];
|
|
185
|
+
if (mapped) { out += String.fromCharCode(mapped); continue; }
|
|
186
|
+
// Common shapes that would otherwise become "?" for no good reason.
|
|
187
|
+
if (cp === 0x00A0) { out += " "; continue; }
|
|
188
|
+
if (cp >= 0x2000 && cp <= 0x200A) { out += " "; continue; }
|
|
189
|
+
if (cp === 0x2212) { out += "-"; continue; }
|
|
190
|
+
out += "?";
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function escapeString(str) {
|
|
195
|
+
return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ------------------------------------------------------------- PDF writer
|
|
199
|
+
function PdfWriter() {
|
|
200
|
+
this.objects = []; // 1-based; objects[i] is the body of object i+1
|
|
201
|
+
}
|
|
202
|
+
PdfWriter.prototype.alloc = function () { this.objects.push(null); return this.objects.length; };
|
|
203
|
+
PdfWriter.prototype.set = function (id, body) { this.objects[id - 1] = body; };
|
|
204
|
+
PdfWriter.prototype.add = function (body) { var id = this.alloc(); this.set(id, body); return id; };
|
|
205
|
+
|
|
206
|
+
// Assembled as BYTES, not as a string: a content stream can hold binary
|
|
207
|
+
// (an embedded JPEG), and /Length plus the xref offsets are byte counts. A
|
|
208
|
+
// string-based assembler is correct right up until the first image, then
|
|
209
|
+
// silently produces a file no reader will open.
|
|
210
|
+
PdfWriter.prototype.build = function (rootId, infoId) {
|
|
211
|
+
var chunks = [], length = 0;
|
|
212
|
+
function push(part) {
|
|
213
|
+
var bytes = (typeof part === "string") ? latin1Bytes(part) : part;
|
|
214
|
+
chunks.push(bytes); length += bytes.length;
|
|
215
|
+
return length;
|
|
216
|
+
}
|
|
217
|
+
push("%PDF-1.4\n");
|
|
218
|
+
// A binary comment marks the file as binary for transfer agents.
|
|
219
|
+
push(new Uint8Array([0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A]));
|
|
220
|
+
|
|
221
|
+
var offsets = [];
|
|
222
|
+
for (var i = 0; i < this.objects.length; i++) {
|
|
223
|
+
offsets[i] = length;
|
|
224
|
+
push((i + 1) + " 0 obj\n");
|
|
225
|
+
var body = this.objects[i];
|
|
226
|
+
if (body && body.stream) {
|
|
227
|
+
var data = (typeof body.stream === "string") ? latin1Bytes(body.stream) : body.stream;
|
|
228
|
+
push(body.dict.replace("/Length 0", "/Length " + data.length) + "\nstream\n");
|
|
229
|
+
push(data);
|
|
230
|
+
push("\nendstream\n");
|
|
231
|
+
} else {
|
|
232
|
+
push(body + "\n");
|
|
233
|
+
}
|
|
234
|
+
push("endobj\n");
|
|
235
|
+
}
|
|
236
|
+
var xrefAt = length;
|
|
237
|
+
var xref = "xref\n0 " + (this.objects.length + 1) + "\n0000000000 65535 f \n";
|
|
238
|
+
for (var j = 0; j < offsets.length; j++) {
|
|
239
|
+
xref += pad10(offsets[j]) + " 00000 n \n";
|
|
240
|
+
}
|
|
241
|
+
push(xref);
|
|
242
|
+
push("trailer\n<< /Size " + (this.objects.length + 1) + " /Root " + rootId + " 0 R /Info " + infoId + " 0 R >>\n");
|
|
243
|
+
push("startxref\n" + xrefAt + "\n%%EOF\n");
|
|
244
|
+
|
|
245
|
+
var out = new Uint8Array(length), at = 0;
|
|
246
|
+
for (var k = 0; k < chunks.length; k++) { out.set(chunks[k], at); at += chunks[k].length; }
|
|
247
|
+
return out;
|
|
248
|
+
};
|
|
249
|
+
function pad10(n) { var s = String(n); while (s.length < 10) s = "0" + s; return s; }
|
|
250
|
+
function latin1Bytes(str) {
|
|
251
|
+
var out = new Uint8Array(str.length);
|
|
252
|
+
for (var i = 0; i < str.length; i++) out[i] = str.charCodeAt(i) & 0xFF;
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------- layout
|
|
257
|
+
//
|
|
258
|
+
// A flat list of positioned draw operations is produced first, then paged.
|
|
259
|
+
// Keeping measurement and pagination apart is what makes "keep this heading
|
|
260
|
+
// with the paragraph under it" and "repeat a table header row" possible at
|
|
261
|
+
// all; doing both in one pass forces every decision to be final immediately.
|
|
262
|
+
|
|
263
|
+
function styleOf(node, inherited) {
|
|
264
|
+
var s = {
|
|
265
|
+
bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
|
|
266
|
+
strike: inherited.strike, size: inherited.size, color: inherited.color,
|
|
267
|
+
mono: inherited.mono, link: inherited.link
|
|
268
|
+
};
|
|
269
|
+
var tag = node.tagName ? node.tagName.toLowerCase() : "";
|
|
270
|
+
if (tag === "b" || tag === "strong") s.bold = true;
|
|
271
|
+
if (tag === "i" || tag === "em") s.italic = true;
|
|
272
|
+
if (tag === "u" || tag === "ins") s.underline = true;
|
|
273
|
+
if (tag === "s" || tag === "strike" || tag === "del") s.strike = true;
|
|
274
|
+
if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
|
|
275
|
+
if (tag === "a" && node.getAttribute("href")) { s.link = node.getAttribute("href"); s.color = [0, 0.33, 0.8]; s.underline = true; }
|
|
276
|
+
if (tag === "sup" || tag === "sub") s.size = s.size * 0.75;
|
|
277
|
+
|
|
278
|
+
var css = node.style;
|
|
279
|
+
if (css) {
|
|
280
|
+
if (/bold|^[6-9]00$/.test(css.fontWeight || "")) s.bold = true;
|
|
281
|
+
if (css.fontStyle === "italic" || css.fontStyle === "oblique") s.italic = true;
|
|
282
|
+
if ((css.textDecorationLine || css.textDecoration || "").indexOf("underline") >= 0) s.underline = true;
|
|
283
|
+
if ((css.textDecorationLine || css.textDecoration || "").indexOf("line-through") >= 0) s.strike = true;
|
|
284
|
+
if (css.color) { var c = parseColor(css.color); if (c) s.color = c; }
|
|
285
|
+
if (css.fontSize) { var fs = toPoints(css.fontSize, null); if (fs) s.size = fs; }
|
|
286
|
+
}
|
|
287
|
+
return s;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseColor(value) {
|
|
291
|
+
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.replace(/\s/g, ""));
|
|
292
|
+
if (m) {
|
|
293
|
+
var hex = m[1];
|
|
294
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
295
|
+
return [parseInt(hex.substr(0, 2), 16) / 255, parseInt(hex.substr(2, 2), 16) / 255, parseInt(hex.substr(4, 2), 16) / 255];
|
|
296
|
+
}
|
|
297
|
+
var rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value);
|
|
298
|
+
if (rgb) return [rgb[1] / 255, rgb[2] / 255, rgb[3] / 255];
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var HEADING_SCALE = { h1: 2, h2: 1.6, h3: 1.35, h4: 1.15, h5: 1, h6: 0.9 };
|
|
303
|
+
|
|
304
|
+
function Layout(geom, baseSize, familyKey) {
|
|
305
|
+
this.geom = geom;
|
|
306
|
+
this.baseSize = baseSize;
|
|
307
|
+
this.family = familyKey;
|
|
308
|
+
this.contentWidth = geom.width - geom.margin.left - geom.margin.right;
|
|
309
|
+
this.blocks = []; // { type, ... } in document order
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Collect the inline runs of a block, splitting into words for wrapping.
|
|
313
|
+
Layout.prototype.runsOf = function (node, inherited) {
|
|
314
|
+
var runs = [];
|
|
315
|
+
var self = this;
|
|
316
|
+
(function walk(n, style) {
|
|
317
|
+
for (var i = 0; i < n.childNodes.length; i++) {
|
|
318
|
+
var c = n.childNodes[i];
|
|
319
|
+
if (c.nodeType === 3) {
|
|
320
|
+
var text = (c.nodeValue || "").replace(/\s+/g, " ");
|
|
321
|
+
if (text) runs.push({ text: text, style: style });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (c.nodeType !== 1) continue;
|
|
325
|
+
var tag = c.tagName.toLowerCase();
|
|
326
|
+
if (tag === "br") { runs.push({ br: true, style: style }); continue; }
|
|
327
|
+
if (tag === "img") { runs.push({ img: c, style: style }); continue; }
|
|
328
|
+
if (tag === "script" || tag === "style") continue;
|
|
329
|
+
walk(c, styleOf(c, style));
|
|
330
|
+
}
|
|
331
|
+
})(node, inherited);
|
|
332
|
+
return runs;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Break runs into laid-out lines that fit `width`.
|
|
336
|
+
Layout.prototype.wrap = function (runs, width, lineHeightFactor) {
|
|
337
|
+
var lines = [], line = [], used = 0, self = this;
|
|
338
|
+
var maxSize = this.baseSize;
|
|
339
|
+
|
|
340
|
+
function flush() {
|
|
341
|
+
// Trailing spaces must not count toward a line's width, or a
|
|
342
|
+
// right-aligned or centred line drifts by the width of the space.
|
|
343
|
+
while (line.length && /^\s+$/.test(line[line.length - 1].text || "")) line.pop();
|
|
344
|
+
lines.push({ parts: line, width: used, size: maxSize });
|
|
345
|
+
line = []; used = 0; maxSize = self.baseSize;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (var r = 0; r < runs.length; r++) {
|
|
349
|
+
var run = runs[r];
|
|
350
|
+
if (run.br) { flush(); continue; }
|
|
351
|
+
if (run.img) {
|
|
352
|
+
var dims = imageDims(run.img, width);
|
|
353
|
+
if (line.length) flush();
|
|
354
|
+
lines.push({ parts: [{ img: run.img, w: dims.w, h: dims.h, style: run.style }], width: dims.w, size: dims.h, image: true });
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
var styleIndex = (run.style.bold ? 1 : 0) | (run.style.italic ? 2 : 0);
|
|
358
|
+
var family = run.style.mono ? "courier" : this.family;
|
|
359
|
+
// Split keeping the separators, so a space that ends a line can be
|
|
360
|
+
// dropped while a space between two words on the same line is kept.
|
|
361
|
+
var tokens = run.text.split(/(\s+)/);
|
|
362
|
+
for (var t = 0; t < tokens.length; t++) {
|
|
363
|
+
var tok = tokens[t];
|
|
364
|
+
if (!tok) continue;
|
|
365
|
+
var w = textWidth(toWinAnsi(tok), family, styleIndex, run.style.size);
|
|
366
|
+
if (used + w > width && line.length && !/^\s+$/.test(tok)) {
|
|
367
|
+
flush();
|
|
368
|
+
// A wrapped line never starts with the space that caused it.
|
|
369
|
+
if (/^\s+$/.test(tok)) continue;
|
|
370
|
+
}
|
|
371
|
+
line.push({ text: tok, style: run.style, family: family, styleIndex: styleIndex, w: w });
|
|
372
|
+
used += w;
|
|
373
|
+
if (run.style.size > maxSize) maxSize = run.style.size;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (line.length) flush();
|
|
377
|
+
if (!lines.length) lines.push({ parts: [], width: 0, size: this.baseSize });
|
|
378
|
+
for (var i = 0; i < lines.length; i++) lines[i].height = lines[i].image ? lines[i].size : lines[i].size * lineHeightFactor;
|
|
379
|
+
return lines;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
function imageDims(img, maxWidth) {
|
|
383
|
+
var w = img.naturalWidth || img.width || 0;
|
|
384
|
+
var h = img.naturalHeight || img.height || 0;
|
|
385
|
+
if (!w || !h) return { w: 0, h: 0 };
|
|
386
|
+
var pw = w * 0.75, ph = h * 0.75; // CSS px -> points
|
|
387
|
+
if (pw > maxWidth) { ph = ph * maxWidth / pw; pw = maxWidth; }
|
|
388
|
+
return { w: pw, h: ph };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ------------------------------------------------------------ block walk
|
|
392
|
+
Layout.prototype.run = function (root) {
|
|
393
|
+
var self = this;
|
|
394
|
+
var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
|
|
395
|
+
|
|
396
|
+
function block(node, style, indent, listMarker, listId, listOrdered) {
|
|
397
|
+
var tag = node.tagName.toLowerCase();
|
|
398
|
+
var s = styleOf(node, style);
|
|
399
|
+
var align = "left";
|
|
400
|
+
var ta = (node.style && node.style.textAlign) || "";
|
|
401
|
+
if (ta === "center" || ta === "right" || ta === "justify") align = ta;
|
|
402
|
+
|
|
403
|
+
if (HEADING_SCALE[tag]) { s.size = self.baseSize * HEADING_SCALE[tag]; s.bold = true; }
|
|
404
|
+
if (tag === "pre") s.mono = true;
|
|
405
|
+
|
|
406
|
+
var width = self.contentWidth - indent;
|
|
407
|
+
var runs = self.runsOf(node, s);
|
|
408
|
+
var lines = self.wrap(runs, width, tag === "pre" ? 1.25 : 1.4);
|
|
409
|
+
self.blocks.push({
|
|
410
|
+
type: "text", lines: lines, indent: indent, align: align,
|
|
411
|
+
spaceBefore: HEADING_SCALE[tag] ? s.size * 0.6 : self.baseSize * 0.35,
|
|
412
|
+
spaceAfter: HEADING_SCALE[tag] ? s.size * 0.3 : self.baseSize * 0.35,
|
|
413
|
+
marker: listMarker || null, markerStyle: s,
|
|
414
|
+
// A heading alone at the foot of a page is the classic ugly
|
|
415
|
+
// break; it is pinned to what follows it.
|
|
416
|
+
keepWithNext: !!HEADING_SCALE[tag],
|
|
417
|
+
rule: tag === "blockquote" ? "quote" : null,
|
|
418
|
+
// Semantics for the tag tree. A PDF whose text is selectable but
|
|
419
|
+
// untagged still fails an accessibility audit: assistive
|
|
420
|
+
// technology gets a flat stream with no headings, no list
|
|
421
|
+
// structure and no reliable reading order.
|
|
422
|
+
role: HEADING_SCALE[tag] ? "H" + tag.charAt(1) : (tag === "blockquote" ? "BlockQuote" : "P"),
|
|
423
|
+
listId: listId || null, listOrdered: !!listOrdered
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function walk(node, style, indent) {
|
|
428
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
429
|
+
var c = node.childNodes[i];
|
|
430
|
+
if (c.nodeType === 3) {
|
|
431
|
+
if (!(c.nodeValue || "").trim()) continue;
|
|
432
|
+
// Loose text directly under the root still deserves a block.
|
|
433
|
+
self.blocks.push({ type: "text", lines: self.wrap([{ text: c.nodeValue.replace(/\s+/g, " "), style: style }], self.contentWidth - indent, 1.4), indent: indent, align: "left", spaceBefore: 0, spaceAfter: self.baseSize * 0.35 });
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (c.nodeType !== 1) continue;
|
|
437
|
+
var tag = c.tagName.toLowerCase();
|
|
438
|
+
if (tag === "script" || tag === "style" || tag === "noscript") continue;
|
|
439
|
+
|
|
440
|
+
if (c.getAttribute && c.getAttribute("data-rte-page-break") !== null) { self.blocks.push({ type: "pagebreak" }); continue; }
|
|
441
|
+
if (c.style && /always|page/.test(c.style.pageBreakBefore || c.style.breakBefore || "")) self.blocks.push({ type: "pagebreak" });
|
|
442
|
+
|
|
443
|
+
if (tag === "hr") { self.blocks.push({ type: "rule", indent: indent }); continue; }
|
|
444
|
+
if (tag === "ul" || tag === "ol") {
|
|
445
|
+
var counter = parseInt(c.getAttribute("start") || "1", 10) || 1;
|
|
446
|
+
// Every item of one list shares an id so the tag tree can
|
|
447
|
+
// rebuild a single <L> around them; without it each item
|
|
448
|
+
// becomes an isolated paragraph and a screen reader never
|
|
449
|
+
// announces "list, 4 items".
|
|
450
|
+
var listId = "L" + (self.listSeq = (self.listSeq || 0) + 1);
|
|
451
|
+
for (var li = 0; li < c.childNodes.length; li++) {
|
|
452
|
+
var item = c.childNodes[li];
|
|
453
|
+
if (item.nodeType !== 1 || item.tagName.toLowerCase() !== "li") continue;
|
|
454
|
+
var marker = tag === "ol" ? (counter++) + "." : "•";
|
|
455
|
+
// The item's own text, then any nested list one level in.
|
|
456
|
+
block(item, styleOf(item, style), indent + self.baseSize * 1.6, marker, listId, tag === "ol");
|
|
457
|
+
walkNestedLists(item, styleOf(item, style), indent + self.baseSize * 1.6);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (tag === "table") { self.table(c, styleOf(c, style), indent); continue; }
|
|
462
|
+
if (tag === "img") {
|
|
463
|
+
var dims = imageDims(c, self.contentWidth - indent);
|
|
464
|
+
if (dims.w) self.blocks.push({ type: "text", role: "Figure", alt: c.getAttribute("alt") || "", lines: [{ parts: [{ img: c, w: dims.w, h: dims.h, style: style }], width: dims.w, size: dims.h, height: dims.h, image: true }], indent: indent, align: "left", spaceBefore: 4, spaceAfter: 4 });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (isBlockTag(tag)) {
|
|
468
|
+
if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0)); continue; }
|
|
469
|
+
block(c, style, indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0), null);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
// Inline content sitting directly under a container.
|
|
473
|
+
block(c, style, indent, null);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function walkNestedLists(item, style, indent) {
|
|
478
|
+
for (var i = 0; i < item.childNodes.length; i++) {
|
|
479
|
+
var c = item.childNodes[i];
|
|
480
|
+
if (c.nodeType !== 1) continue;
|
|
481
|
+
var tag = c.tagName.toLowerCase();
|
|
482
|
+
if (tag === "ul" || tag === "ol") walk({ childNodes: [c] }, style, indent);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
walk(root, base, 0);
|
|
487
|
+
return this.blocks;
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
function isBlockTag(tag) {
|
|
491
|
+
return /^(p|div|h[1-6]|blockquote|pre|section|article|header|footer|main|aside|figure|figcaption|dl|dd|dt|li|address)$/.test(tag);
|
|
492
|
+
}
|
|
493
|
+
function hasBlockChildren(node) {
|
|
494
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
495
|
+
var c = node.childNodes[i];
|
|
496
|
+
if (c.nodeType === 1 && (isBlockTag(c.tagName.toLowerCase()) || /^(ul|ol|table|hr)$/.test(c.tagName.toLowerCase()))) return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
Layout.prototype.table = function (node, style, indent) {
|
|
502
|
+
var self = this;
|
|
503
|
+
var rows = [];
|
|
504
|
+
var trs = node.getElementsByTagName("tr");
|
|
505
|
+
var maxCells = 0;
|
|
506
|
+
for (var r = 0; r < trs.length; r++) {
|
|
507
|
+
var cells = [];
|
|
508
|
+
for (var c = 0; c < trs[r].childNodes.length; c++) {
|
|
509
|
+
var cell = trs[r].childNodes[c];
|
|
510
|
+
if (cell.nodeType !== 1) continue;
|
|
511
|
+
var tag = cell.tagName.toLowerCase();
|
|
512
|
+
if (tag !== "td" && tag !== "th") continue;
|
|
513
|
+
cells.push({ node: cell, header: tag === "th" });
|
|
514
|
+
}
|
|
515
|
+
if (cells.length) { rows.push(cells); if (cells.length > maxCells) maxCells = cells.length; }
|
|
516
|
+
}
|
|
517
|
+
if (!rows.length) return;
|
|
518
|
+
|
|
519
|
+
var width = this.contentWidth - indent;
|
|
520
|
+
var colWidth = width / maxCells;
|
|
521
|
+
var pad = 4;
|
|
522
|
+
var laid = [];
|
|
523
|
+
for (var i = 0; i < rows.length; i++) {
|
|
524
|
+
var out = [], tallest = 0;
|
|
525
|
+
for (var j = 0; j < rows[i].length; j++) {
|
|
526
|
+
var cs = styleOf(rows[i][j].node, style);
|
|
527
|
+
if (rows[i][j].header) cs.bold = true;
|
|
528
|
+
var lines = self.wrap(self.runsOf(rows[i][j].node, cs), colWidth - pad * 2, 1.35);
|
|
529
|
+
var h = 0;
|
|
530
|
+
for (var k = 0; k < lines.length; k++) h += lines[k].height;
|
|
531
|
+
if (h > tallest) tallest = h;
|
|
532
|
+
out.push({ lines: lines, header: rows[i][j].header });
|
|
533
|
+
}
|
|
534
|
+
laid.push({ cells: out, height: tallest + pad * 2, header: rows[i][0] && rows[i][0].header });
|
|
535
|
+
}
|
|
536
|
+
this.blocks.push({
|
|
537
|
+
type: "table", rows: laid, colWidth: colWidth, cols: maxCells,
|
|
538
|
+
indent: indent, pad: pad,
|
|
539
|
+
spaceBefore: this.baseSize * 0.5, spaceAfter: this.baseSize * 0.5
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// ------------------------------------------------------------- rendering
|
|
544
|
+
function buildPdf(options) {
|
|
545
|
+
var familyKey = String(options.font || config.pdfExportFont || "helvetica").toLowerCase();
|
|
546
|
+
if (!FONT_FAMILIES[familyKey]) familyKey = "helvetica";
|
|
547
|
+
var baseSize = Number(options.fontSize || config.pdfExportBaseFontSize || 11);
|
|
548
|
+
if (!isFinite(baseSize) || baseSize <= 0) baseSize = 11;
|
|
549
|
+
var geom = pageGeometry(options);
|
|
550
|
+
|
|
551
|
+
// getHTMLCode() is the SERIALIZED document: presentational plugins strip
|
|
552
|
+
// their own chrome from it. Reading the live DOM instead would bake page
|
|
553
|
+
// overlays, watermarks and formatting marks into the PDF.
|
|
554
|
+
var host = document.createElement("div");
|
|
555
|
+
host.innerHTML = editor.getHTMLCode() || "";
|
|
556
|
+
|
|
557
|
+
var layout = new Layout(geom, baseSize, familyKey);
|
|
558
|
+
var blocks = layout.run(host);
|
|
559
|
+
|
|
560
|
+
var pages = paginate(blocks, geom, baseSize);
|
|
561
|
+
return emit(pages, geom, familyKey, baseSize, options);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function paginate(blocks, geom, baseSize) {
|
|
565
|
+
var usableTop = geom.height - geom.margin.top;
|
|
566
|
+
var usableBottom = geom.margin.bottom;
|
|
567
|
+
var pages = [], current = [], y = usableTop;
|
|
568
|
+
|
|
569
|
+
function newPage() { pages.push(current); current = []; y = usableTop; }
|
|
570
|
+
|
|
571
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
572
|
+
var b = blocks[i];
|
|
573
|
+
if (b.type === "pagebreak") { if (current.length) newPage(); continue; }
|
|
574
|
+
|
|
575
|
+
if (b.type === "rule") {
|
|
576
|
+
if (y - baseSize < usableBottom) newPage();
|
|
577
|
+
y -= baseSize * 0.6;
|
|
578
|
+
current.push({ op: "rule", y: y, indent: b.indent });
|
|
579
|
+
y -= baseSize * 0.6;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (b.type === "table") {
|
|
584
|
+
y -= b.spaceBefore;
|
|
585
|
+
for (var r = 0; r < b.rows.length; r++) {
|
|
586
|
+
var row = b.rows[r];
|
|
587
|
+
if (y - row.height < usableBottom) {
|
|
588
|
+
newPage();
|
|
589
|
+
// Repeat the header row on the continuation page, or the
|
|
590
|
+
// rest of the table arrives as unlabelled numbers.
|
|
591
|
+
if (b.rows[0] && b.rows[0].header && r > 0) {
|
|
592
|
+
current.push({ op: "row", row: b.rows[0], y: y, block: b });
|
|
593
|
+
y -= b.rows[0].height;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
current.push({ op: "row", row: row, y: y, block: b });
|
|
597
|
+
y -= row.height;
|
|
598
|
+
}
|
|
599
|
+
y -= b.spaceAfter;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
y -= b.spaceBefore;
|
|
604
|
+
for (var l = 0; l < b.lines.length; l++) {
|
|
605
|
+
var line = b.lines[l];
|
|
606
|
+
if (y - line.height < usableBottom) newPage();
|
|
607
|
+
current.push({ op: "line", line: line, y: y, block: b, first: l === 0 });
|
|
608
|
+
y -= line.height;
|
|
609
|
+
}
|
|
610
|
+
y -= b.spaceAfter;
|
|
611
|
+
|
|
612
|
+
// keepWithNext: if a heading ended up as the last thing on the page,
|
|
613
|
+
// move it to the next one rather than orphaning it.
|
|
614
|
+
if (b.keepWithNext && current.length && blocks[i + 1] && blocks[i + 1].type !== "pagebreak") {
|
|
615
|
+
var nextHeight = estimateFirstLine(blocks[i + 1]);
|
|
616
|
+
if (y - nextHeight < usableBottom) {
|
|
617
|
+
var moved = [];
|
|
618
|
+
while (current.length && current[current.length - 1].block === b) moved.unshift(current.pop());
|
|
619
|
+
if (current.length) {
|
|
620
|
+
newPage();
|
|
621
|
+
for (var m = 0; m < moved.length; m++) {
|
|
622
|
+
moved[m].y = y;
|
|
623
|
+
current.push(moved[m]);
|
|
624
|
+
y -= moved[m].line.height;
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
// Already at the top of a page: nothing to gain by moving.
|
|
628
|
+
for (var m2 = 0; m2 < moved.length; m2++) current.push(moved[m2]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (current.length || !pages.length) pages.push(current);
|
|
634
|
+
return pages;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function estimateFirstLine(block) {
|
|
638
|
+
if (block.type === "table") return block.rows[0] ? block.rows[0].height : 0;
|
|
639
|
+
if (block.lines && block.lines[0]) return block.lines[0].height;
|
|
640
|
+
return 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function emit(pages, geom, familyKey, baseSize, options) {
|
|
644
|
+
var pdf = new PdfWriter();
|
|
645
|
+
var fam = FONT_FAMILIES[familyKey];
|
|
646
|
+
var mono = FONT_FAMILIES.courier;
|
|
647
|
+
|
|
648
|
+
// Font objects: four faces of the body family plus four of Courier, each
|
|
649
|
+
// carrying the widths this layout was computed with.
|
|
650
|
+
var fontIds = {}, fontRes = [];
|
|
651
|
+
function addFont(name, key, styleIndex, alias) {
|
|
652
|
+
var widths = widthsFor(key, styleIndex);
|
|
653
|
+
var id = pdf.add("<< /Type /Font /Subtype /Type1 /BaseFont /" + name +
|
|
654
|
+
" /Encoding /WinAnsiEncoding /FirstChar " + FIRST_CHAR + " /LastChar " + LAST_CHAR +
|
|
655
|
+
" /Widths [" + widths.join(" ") + "] >>");
|
|
656
|
+
fontIds[alias] = id;
|
|
657
|
+
fontRes.push("/" + alias + " " + id + " 0 R");
|
|
658
|
+
}
|
|
659
|
+
for (var s = 0; s < 4; s++) {
|
|
660
|
+
addFont(fam.faces[s], familyKey, s, "F" + s);
|
|
661
|
+
addFont(mono.faces[s], "courier", s, "M" + s);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
var images = {}, imageRes = [];
|
|
665
|
+
function imageRef(img) {
|
|
666
|
+
var src = img.getAttribute("src") || "";
|
|
667
|
+
if (images[src]) return images[src];
|
|
668
|
+
var data = encodeImage(img);
|
|
669
|
+
if (!data) return null;
|
|
670
|
+
var id = pdf.add({
|
|
671
|
+
dict: "<< /Type /XObject /Subtype /Image /Width " + data.width + " /Height " + data.height +
|
|
672
|
+
" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length 0 >>",
|
|
673
|
+
stream: data.bytes
|
|
674
|
+
});
|
|
675
|
+
var alias = "Im" + (imageRes.length + 1);
|
|
676
|
+
images[src] = { id: id, alias: alias };
|
|
677
|
+
imageRes.push("/" + alias + " " + id + " 0 R");
|
|
678
|
+
return images[src];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
var pageIds = [], contentIds = [], annotIds = [];
|
|
682
|
+
for (var p = 0; p < pages.length; p++) {
|
|
683
|
+
pageIds.push(pdf.alloc());
|
|
684
|
+
contentIds.push(pdf.alloc());
|
|
685
|
+
annotIds.push([]);
|
|
686
|
+
}
|
|
687
|
+
var pagesId = pdf.alloc();
|
|
688
|
+
|
|
689
|
+
var allMarks = [];
|
|
690
|
+
for (var pi = 0; pi < pages.length; pi++) {
|
|
691
|
+
var tagger = new Tagger(pi);
|
|
692
|
+
var draw = renderPage(pages[pi], geom, familyKey, baseSize, imageRef, pi + 1, pages.length, options, tagger);
|
|
693
|
+
pdf.set(contentIds[pi], { dict: "<< /Length 0 >>", stream: draw.content });
|
|
694
|
+
allMarks.push(draw.marks);
|
|
695
|
+
for (var a = 0; a < draw.links.length; a++) {
|
|
696
|
+
var lk = draw.links[a];
|
|
697
|
+
annotIds[pi].push(pdf.add("<< /Type /Annot /Subtype /Link /Rect [" +
|
|
698
|
+
fixed(lk.x0) + " " + fixed(lk.y0) + " " + fixed(lk.x1) + " " + fixed(lk.y1) +
|
|
699
|
+
"] /Border [0 0 0] /A << /S /URI /URI (" + escapeString(toWinAnsi(lk.href)) + ") >> >>"));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
var struct = buildStructTree(pdf, allMarks, pageIds);
|
|
704
|
+
|
|
705
|
+
for (var pj = 0; pj < pages.length; pj++) {
|
|
706
|
+
pdf.set(pageIds[pj], "<< /Type /Page /Parent " + pagesId + " 0 R /MediaBox [0 0 " +
|
|
707
|
+
fixed(geom.width) + " " + fixed(geom.height) + "] /Resources << /Font << " + fontRes.join(" ") + " >>" +
|
|
708
|
+
(imageRes.length ? " /XObject << " + imageRes.join(" ") + " >>" : "") +
|
|
709
|
+
" >> /Contents " + contentIds[pj] + " 0 R /StructParents " + pj +
|
|
710
|
+
(annotIds[pj].length ? " /Annots [" + annotIds[pj].map(function (id) { return id + " 0 R"; }).join(" ") + "]" : "") +
|
|
711
|
+
" >>");
|
|
712
|
+
}
|
|
713
|
+
pdf.set(pagesId, "<< /Type /Pages /Count " + pages.length + " /Kids [" +
|
|
714
|
+
pageIds.map(function (id) { return id + " 0 R"; }).join(" ") + "] >>");
|
|
715
|
+
|
|
716
|
+
var title = options.title || config.pdfExportTitle || document.title || "Document";
|
|
717
|
+
var author = options.author || config.pdfExportAuthor || "";
|
|
718
|
+
// /MarkInfo and /StructTreeRoot declare the file tagged. DisplayDocTitle
|
|
719
|
+
// makes the viewer show the document's title rather than its filename,
|
|
720
|
+
// which PDF/UA requires and which is the difference between a screen
|
|
721
|
+
// reader announcing "Quarterly Report" and "Export-260802-1431.pdf".
|
|
722
|
+
var catalogId = pdf.add("<< /Type /Catalog /Pages " + pagesId + " 0 R" +
|
|
723
|
+
" /Lang (" + escapeString(String(options.lang || document.documentElement.lang || "en")) + ")" +
|
|
724
|
+
" /MarkInfo << /Marked true >>" +
|
|
725
|
+
" /StructTreeRoot " + struct.rootId + " 0 R" +
|
|
726
|
+
" /ViewerPreferences << /DisplayDocTitle true >> >>");
|
|
727
|
+
var infoId = pdf.add("<< /Title (" + escapeString(toWinAnsi(String(title))) + ")" +
|
|
728
|
+
(author ? " /Author (" + escapeString(toWinAnsi(String(author))) + ")" : "") +
|
|
729
|
+
" /Producer (RichTextEditor) /Creator (RichTextEditor) /CreationDate (" + pdfDate(new Date()) + ") >>");
|
|
730
|
+
|
|
731
|
+
return pdf.build(catalogId, infoId);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Turn the per-page marked-content records into a structure tree.
|
|
735
|
+
//
|
|
736
|
+
// The tree is built from the marks rather than from the block list because a
|
|
737
|
+
// single block can be split across pages: its structure element then holds
|
|
738
|
+
// several marked-content references, each naming the page it lives on.
|
|
739
|
+
function buildStructTree(pdf, allMarks, pageIds) {
|
|
740
|
+
var rootId = pdf.alloc();
|
|
741
|
+
var nodes = []; // { id, role, kids, extra, children, parent }
|
|
742
|
+
var parentTree = []; // per page: array of struct ids by MCID
|
|
743
|
+
|
|
744
|
+
function node(role, extra) {
|
|
745
|
+
var n = { id: pdf.alloc(), role: role, kids: [], extra: extra || "", children: [], parent: null };
|
|
746
|
+
nodes.push(n);
|
|
747
|
+
return n;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var docNode = node("Document");
|
|
751
|
+
var listNodes = new Map(); // listId -> L node
|
|
752
|
+
var itemNodes = new Map(); // block -> LI node
|
|
753
|
+
var labelNodes = new Map(); // block -> Lbl node
|
|
754
|
+
var bodyNodes = new Map(); // block -> P / LBody / Figure node
|
|
755
|
+
var tableNodes = new Map(); // block -> Table node
|
|
756
|
+
var rowNodes = new Map(); // row -> TR node
|
|
757
|
+
var cellNodes = new Map(); // row -> { col -> TD/TH node }
|
|
758
|
+
|
|
759
|
+
for (var p = 0; p < allMarks.length; p++) {
|
|
760
|
+
var marks = allMarks[p];
|
|
761
|
+
parentTree[p] = [];
|
|
762
|
+
for (var m = 0; m < marks.length; m++) {
|
|
763
|
+
var owner = marks[m].owner, mcid = marks[m].mcid;
|
|
764
|
+
var target = resolve(owner);
|
|
765
|
+
target.kids.push({ page: p, mcid: mcid });
|
|
766
|
+
parentTree[p][mcid] = target.id;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// The parent is recorded when the child is attached. Searching for it
|
|
771
|
+
// afterwards would be a scan of every node for every node.
|
|
772
|
+
function attach(parent, child) {
|
|
773
|
+
if (child.parent) return;
|
|
774
|
+
child.parent = parent;
|
|
775
|
+
parent.children.push(child);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function resolve(owner) {
|
|
779
|
+
if (owner.kind === "cell") {
|
|
780
|
+
var table = tableNodes.get(owner.block);
|
|
781
|
+
if (!table) { table = node("Table"); tableNodes.set(owner.block, table); attach(docNode, table); }
|
|
782
|
+
var row = rowNodes.get(owner.row);
|
|
783
|
+
if (!row) { row = node("TR"); rowNodes.set(owner.row, row); attach(table, row); }
|
|
784
|
+
var perRow = cellNodes.get(owner.row);
|
|
785
|
+
if (!perRow) { perRow = {}; cellNodes.set(owner.row, perRow); }
|
|
786
|
+
if (!perRow[owner.col]) {
|
|
787
|
+
// A header cell must be TH, not TD: it is what lets a screen
|
|
788
|
+
// reader announce "Revenue, 1,240" instead of just "1,240".
|
|
789
|
+
perRow[owner.col] = node(owner.header ? "TH" : "TD", owner.header ? " /Scope /Column" : "");
|
|
790
|
+
attach(row, perRow[owner.col]);
|
|
791
|
+
}
|
|
792
|
+
return perRow[owner.col];
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
var block = owner.block;
|
|
796
|
+
var container = docNode;
|
|
797
|
+
if (block.listId) {
|
|
798
|
+
var listNode = listNodes.get(block.listId);
|
|
799
|
+
if (!listNode) { listNode = node("L"); listNodes.set(block.listId, listNode); attach(docNode, listNode); }
|
|
800
|
+
var itemNode = itemNodes.get(block);
|
|
801
|
+
if (!itemNode) { itemNode = node("LI"); itemNodes.set(block, itemNode); attach(listNode, itemNode); }
|
|
802
|
+
container = itemNode;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (owner.kind === "lbl") {
|
|
806
|
+
var lbl = labelNodes.get(block);
|
|
807
|
+
if (!lbl) { lbl = node("Lbl"); labelNodes.set(block, lbl); attach(container, lbl); }
|
|
808
|
+
return lbl;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
var body = bodyNodes.get(block);
|
|
812
|
+
if (!body) {
|
|
813
|
+
if (block.listId) {
|
|
814
|
+
body = node("LBody");
|
|
815
|
+
} else {
|
|
816
|
+
// A figure with no alt text is an accessibility failure, but
|
|
817
|
+
// an EMPTY /Alt is a worse one: it tells assistive tech the
|
|
818
|
+
// image is decorative when nobody decided that.
|
|
819
|
+
var extra = block.role === "Figure"
|
|
820
|
+
? " /Alt (" + escapeString(toWinAnsi(block.alt || "Image")) + ")"
|
|
821
|
+
: "";
|
|
822
|
+
body = node(block.role || "P", extra);
|
|
823
|
+
}
|
|
824
|
+
bodyNodes.set(block, body);
|
|
825
|
+
attach(container, body);
|
|
826
|
+
}
|
|
827
|
+
return body;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Emit. /K holds child elements first, then marked-content references.
|
|
831
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
832
|
+
var nd = nodes[n];
|
|
833
|
+
var kids = [];
|
|
834
|
+
for (var c = 0; c < nd.children.length; c++) kids.push(nd.children[c].id + " 0 R");
|
|
835
|
+
for (var k = 0; k < nd.kids.length; k++) {
|
|
836
|
+
kids.push("<< /Type /MCR /Pg " + pageIds[nd.kids[k].page] + " 0 R /MCID " + nd.kids[k].mcid + " >>");
|
|
837
|
+
}
|
|
838
|
+
var parentRef = nd.parent ? nd.parent.id : rootId;
|
|
839
|
+
pdf.set(nd.id, "<< /Type /StructElem /S /" + nd.role + " /P " + parentRef + " 0 R" + nd.extra +
|
|
840
|
+
(kids.length ? " /K [" + kids.join(" ") + "]" : "") + " >>");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ParentTree: a number tree keyed by each page's /StructParents value,
|
|
844
|
+
// whose value is the list of owning structure elements indexed by MCID.
|
|
845
|
+
var numsParts = [];
|
|
846
|
+
for (var pt = 0; pt < parentTree.length; pt++) {
|
|
847
|
+
var arr = parentTree[pt] || [];
|
|
848
|
+
var refs = [];
|
|
849
|
+
for (var q = 0; q < arr.length; q++) refs.push((arr[q] || docNode.id) + " 0 R");
|
|
850
|
+
numsParts.push(pt + " [" + refs.join(" ") + "]");
|
|
851
|
+
}
|
|
852
|
+
var parentTreeId = pdf.add("<< /Nums [" + numsParts.join(" ") + "] >>");
|
|
853
|
+
|
|
854
|
+
pdf.set(rootId, "<< /Type /StructTreeRoot /K [" + docNode.id + " 0 R]" +
|
|
855
|
+
" /ParentTree " + parentTreeId + " 0 R /ParentTreeNextKey " + parentTree.length + " >>");
|
|
856
|
+
|
|
857
|
+
return { rootId: rootId };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function pdfDate(d) {
|
|
861
|
+
function two(n) { return (n < 10 ? "0" : "") + n; }
|
|
862
|
+
return "D:" + d.getFullYear() + two(d.getMonth() + 1) + two(d.getDate()) +
|
|
863
|
+
two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds()) + "Z";
|
|
864
|
+
}
|
|
865
|
+
function fixed(n) { return (Math.round(n * 100) / 100).toString(); }
|
|
866
|
+
|
|
867
|
+
// Marked content for the structure tree.
|
|
868
|
+
//
|
|
869
|
+
// Tagging is what separates "a screen reader can read the words" from "a
|
|
870
|
+
// screen reader knows this is a level-2 heading, followed by a 4-item list,
|
|
871
|
+
// followed by a table with header cells". Section 508 and EN 301 549 both
|
|
872
|
+
// require the second. Purely decorative output (rules, cell borders, the
|
|
873
|
+
// running header) is marked as an Artifact instead, which removes it from
|
|
874
|
+
// the reading order rather than reading "line, line, line" to the user.
|
|
875
|
+
function Tagger(pageIndex) {
|
|
876
|
+
this.pageIndex = pageIndex;
|
|
877
|
+
this.next = 0;
|
|
878
|
+
this.marks = []; // { owner, mcid } in reading order
|
|
879
|
+
}
|
|
880
|
+
Tagger.prototype.open = function (out, role, owner) {
|
|
881
|
+
var mcid = this.next++;
|
|
882
|
+
this.marks.push({ owner: owner, mcid: mcid, role: role });
|
|
883
|
+
out.push("/" + role + " << /MCID " + mcid + " >> BDC");
|
|
884
|
+
return mcid;
|
|
885
|
+
};
|
|
886
|
+
Tagger.prototype.close = function (out) { out.push("EMC"); };
|
|
887
|
+
Tagger.prototype.artifact = function (out, fn) {
|
|
888
|
+
out.push("/Artifact BMC");
|
|
889
|
+
fn();
|
|
890
|
+
out.push("EMC");
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
function renderPage(ops, geom, familyKey, baseSize, imageRef, pageNumber, pageCount, options, tagger) {
|
|
894
|
+
var out = [], links = [];
|
|
895
|
+
var left = geom.margin.left;
|
|
896
|
+
|
|
897
|
+
for (var i = 0; i < ops.length; i++) {
|
|
898
|
+
var op = ops[i];
|
|
899
|
+
if (op.op === "rule") {
|
|
900
|
+
// A horizontal rule is decoration, not content.
|
|
901
|
+
tagger.artifact(out, function () {
|
|
902
|
+
out.push("0.8 0.8 0.8 RG 0.5 w " + fixed(left + op.indent) + " " + fixed(op.y) + " m " +
|
|
903
|
+
fixed(geom.width - geom.margin.right) + " " + fixed(op.y) + " l S");
|
|
904
|
+
});
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
if (op.op === "row") { renderRow(op, geom, out, left, imageRef, links, tagger); continue; }
|
|
908
|
+
renderLine(op, geom, out, left, familyKey, imageRef, links, tagger);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Running header/footer, drawn after the body so they always sit on top.
|
|
912
|
+
// Both are artifacts: repeating them into the reading order on every
|
|
913
|
+
// page is one of the most common findings in a PDF accessibility audit.
|
|
914
|
+
var hf = headerFooter(options);
|
|
915
|
+
if (hf.header || hf.footer) {
|
|
916
|
+
tagger.artifact(out, function () {
|
|
917
|
+
if (hf.header) drawRunning(out, hf.header, geom, geom.height - geom.margin.top * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
|
|
918
|
+
if (hf.footer) drawRunning(out, hf.footer, geom, geom.margin.bottom * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return { content: out.join("\n"), links: links, marks: tagger.marks };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function headerFooter(options) {
|
|
926
|
+
var setup = null;
|
|
927
|
+
try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); } catch (e) {}
|
|
928
|
+
setup = setup || {};
|
|
929
|
+
return {
|
|
930
|
+
header: options.headerText || config.pdfExportHeader || plainText(setup.headerHtml) || "",
|
|
931
|
+
footer: options.footerText || config.pdfExportFooter || plainText(setup.footerHtml) || ""
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function plainText(html) {
|
|
935
|
+
if (!html || typeof html !== "string") return "";
|
|
936
|
+
var d = document.createElement("div");
|
|
937
|
+
d.innerHTML = html;
|
|
938
|
+
return (d.textContent || "").replace(/\s+/g, " ").trim();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function drawRunning(out, template, geom, y, familyKey, size, pageNumber, pageCount) {
|
|
942
|
+
var text = String(template).replace(/\{page\}/g, pageNumber).replace(/\{total\}/g, pageCount);
|
|
943
|
+
var win = toWinAnsi(text);
|
|
944
|
+
var w = textWidth(win, familyKey, 0, size);
|
|
945
|
+
var x = (geom.width - w) / 2;
|
|
946
|
+
out.push("BT /F0 " + fixed(size) + " Tf 0.4 0.4 0.4 rg " + fixed(x) + " " + fixed(y) + " Td (" + escapeString(win) + ") Tj ET");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderLine(op, geom, out, left, familyKey, imageRef, links, tagger) {
|
|
950
|
+
var line = op.line, block = op.block;
|
|
951
|
+
var x = left + (block.indent || 0);
|
|
952
|
+
var avail = geom.width - geom.margin.right - x;
|
|
953
|
+
if (block.align === "center") x += (avail - line.width) / 2;
|
|
954
|
+
else if (block.align === "right") x += avail - line.width;
|
|
955
|
+
|
|
956
|
+
if (block.rule === "quote" && !line.image) {
|
|
957
|
+
// The quote bar is decoration.
|
|
958
|
+
var barX = fixed(left + block.indent - 8);
|
|
959
|
+
var run = function () {
|
|
960
|
+
out.push("0.8 0.8 0.85 RG 2 w " + barX + " " + fixed(op.y - line.height + 4) +
|
|
961
|
+
" m " + barX + " " + fixed(op.y + line.size * 0.8) + " l S");
|
|
962
|
+
};
|
|
963
|
+
if (tagger) tagger.artifact(out, run); else run();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// A list marker belongs to the FIRST line only; repeating it on a wrapped
|
|
967
|
+
// continuation line turns one bullet into several.
|
|
968
|
+
if (op.first && block.marker) {
|
|
969
|
+
var ms = block.markerStyle || { size: line.size, color: [0, 0, 0] };
|
|
970
|
+
var mw = textWidth(block.marker, familyKey, 0, ms.size);
|
|
971
|
+
if (tagger) tagger.open(out, "Lbl", { kind: "lbl", block: block });
|
|
972
|
+
out.push("BT /F0 " + fixed(ms.size) + " Tf " + colorOp(ms.color) + " " +
|
|
973
|
+
fixed(x - mw - 6) + " " + fixed(op.y - line.size * 0.85) + " Td (" + escapeString(toWinAnsi(block.marker)) + ") Tj ET");
|
|
974
|
+
if (tagger) tagger.close(out);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Text of the block itself. Figures carry /Alt on the structure element
|
|
978
|
+
// rather than in the content stream.
|
|
979
|
+
//
|
|
980
|
+
// A table cell renders through this function with a SYNTHETIC block
|
|
981
|
+
// (just indent/align), so the owner has to be the cell descriptor the
|
|
982
|
+
// caller supplied. Passing the synthetic block instead makes every cell
|
|
983
|
+
// a top-level paragraph and the table loses its structure entirely.
|
|
984
|
+
if (tagger) tagger.open(out, line.image ? "Figure" : "Span", op.cell || { kind: "body", block: block });
|
|
985
|
+
|
|
986
|
+
// Wrapping split the text into words, but emitting one BT/Tj block per
|
|
987
|
+
// WORD is wrong twice over: the content stream balloons, and text
|
|
988
|
+
// extractors that infer spacing from separate show-operations can lose
|
|
989
|
+
// the spaces between them, so copying a paragraph out of the PDF yields
|
|
990
|
+
// "onelongrunofwords". Adjacent parts that share a font, size and colour
|
|
991
|
+
// are merged back into a single run, spaces included.
|
|
992
|
+
var segments = [];
|
|
993
|
+
var cursor = x;
|
|
994
|
+
for (var i = 0; i < line.parts.length; i++) {
|
|
995
|
+
var part = line.parts[i];
|
|
996
|
+
if (part.img) { segments.push({ img: part, x: cursor, w: part.w }); cursor += part.w; continue; }
|
|
997
|
+
var st = part.style;
|
|
998
|
+
var alias = (st.mono ? "M" : "F") + part.styleIndex;
|
|
999
|
+
var last = segments.length ? segments[segments.length - 1] : null;
|
|
1000
|
+
var mergeable = last && !last.img && last.alias === alias && last.style.size === st.size &&
|
|
1001
|
+
sameColor(last.style.color, st.color) && last.style.underline === st.underline &&
|
|
1002
|
+
last.style.strike === st.strike && last.style.link === st.link;
|
|
1003
|
+
if (mergeable) { last.text += part.text; last.w += part.w; }
|
|
1004
|
+
else { segments.push({ text: part.text, style: st, alias: alias, x: cursor, w: part.w }); }
|
|
1005
|
+
cursor += part.w;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
var baseline = op.y - line.size * 0.85;
|
|
1009
|
+
for (var s = 0; s < segments.length; s++) {
|
|
1010
|
+
var seg = segments[s];
|
|
1011
|
+
if (seg.img) {
|
|
1012
|
+
var ref = imageRef(seg.img.img);
|
|
1013
|
+
if (ref) {
|
|
1014
|
+
out.push("q " + fixed(seg.w) + " 0 0 " + fixed(seg.img.h) + " " + fixed(seg.x) + " " +
|
|
1015
|
+
fixed(op.y - seg.img.h) + " cm /" + ref.alias + " Do Q");
|
|
1016
|
+
}
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
// A run of pure whitespace still advanced the cursor; drawing it
|
|
1020
|
+
// would only add an empty operation.
|
|
1021
|
+
if (!seg.text || /^\s+$/.test(seg.text)) continue;
|
|
1022
|
+
var sst = seg.style;
|
|
1023
|
+
out.push("BT /" + seg.alias + " " + fixed(sst.size) + " Tf " + colorOp(sst.color) + " " +
|
|
1024
|
+
fixed(seg.x) + " " + fixed(baseline) + " Td (" + escapeString(toWinAnsi(seg.text)) + ") Tj ET");
|
|
1025
|
+
if (sst.underline) {
|
|
1026
|
+
out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline - sst.size * 0.12) +
|
|
1027
|
+
" m " + fixed(seg.x + seg.w) + " " + fixed(baseline - sst.size * 0.12) + " l S");
|
|
1028
|
+
}
|
|
1029
|
+
if (sst.strike) {
|
|
1030
|
+
out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline + sst.size * 0.28) +
|
|
1031
|
+
" m " + fixed(seg.x + seg.w) + " " + fixed(baseline + sst.size * 0.28) + " l S");
|
|
1032
|
+
}
|
|
1033
|
+
if (sst.link) {
|
|
1034
|
+
links.push({ href: sst.link, x0: seg.x, y0: baseline - 2, x1: seg.x + seg.w, y1: baseline + sst.size });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (tagger) tagger.close(out);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function sameColor(a, b) {
|
|
1041
|
+
a = a || [0, 0, 0]; b = b || [0, 0, 0];
|
|
1042
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function renderRow(op, geom, out, left, imageRef, links, tagger) {
|
|
1046
|
+
var block = op.block, row = op.row;
|
|
1047
|
+
var x = left + block.indent;
|
|
1048
|
+
var top = op.y, bottom = op.y - row.height;
|
|
1049
|
+
|
|
1050
|
+
for (var c = 0; c < row.cells.length; c++) {
|
|
1051
|
+
var cellX = x + c * block.colWidth;
|
|
1052
|
+
// Shading and borders are decoration; only the cell TEXT belongs to
|
|
1053
|
+
// the reading order.
|
|
1054
|
+
var paint = function () {
|
|
1055
|
+
if (row.header) {
|
|
1056
|
+
out.push("0.94 0.95 0.97 rg " + fixed(cellX) + " " + fixed(bottom) + " " +
|
|
1057
|
+
fixed(block.colWidth) + " " + fixed(row.height) + " re f");
|
|
1058
|
+
}
|
|
1059
|
+
out.push("0.75 0.78 0.82 RG 0.5 w " + fixed(cellX) + " " + fixed(bottom) + " " +
|
|
1060
|
+
fixed(block.colWidth) + " " + fixed(row.height) + " re S");
|
|
1061
|
+
};
|
|
1062
|
+
if (tagger) tagger.artifact(out, paint); else paint();
|
|
1063
|
+
|
|
1064
|
+
// One structure element per CELL, identified by the row object and
|
|
1065
|
+
// column index so a table split across pages still resolves to the
|
|
1066
|
+
// right TD (and a repeated header row to the right TH).
|
|
1067
|
+
var cellOwner = { kind: "cell", block: block, row: row, col: c, header: !!row.cells[c].header };
|
|
1068
|
+
var cy = top - block.pad;
|
|
1069
|
+
var lines = row.cells[c].lines;
|
|
1070
|
+
for (var l = 0; l < lines.length; l++) {
|
|
1071
|
+
renderLine({ line: lines[l], y: cy, block: { indent: 0, align: "left" }, first: l === 0, cell: cellOwner },
|
|
1072
|
+
{ width: cellX + block.colWidth + geom.margin.right, margin: { right: geom.margin.right } },
|
|
1073
|
+
out, cellX + block.pad, "helvetica", imageRef, links, tagger);
|
|
1074
|
+
cy -= lines[l].height;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function colorOp(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " rg"; }
|
|
1080
|
+
function colorStroke(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " RG"; }
|
|
1081
|
+
|
|
1082
|
+
// Every image is re-encoded to JPEG through a canvas. PNG could be embedded
|
|
1083
|
+
// as FlateDecode without re-encoding, but only for a narrow set of colour
|
|
1084
|
+
// types and never with an alpha channel; routing everything through one path
|
|
1085
|
+
// means GIF, WEBP, BMP and SVG all work rather than only some of them.
|
|
1086
|
+
// Transparent pixels are composited onto white, which is what the page is.
|
|
1087
|
+
function encodeImage(img) {
|
|
1088
|
+
try {
|
|
1089
|
+
var w = img.naturalWidth || img.width, h = img.naturalHeight || img.height;
|
|
1090
|
+
if (!w || !h) return null;
|
|
1091
|
+
var max = 2000;
|
|
1092
|
+
var scale = Math.min(1, max / Math.max(w, h));
|
|
1093
|
+
var cw = Math.max(1, Math.round(w * scale)), ch = Math.max(1, Math.round(h * scale));
|
|
1094
|
+
var canvas = document.createElement("canvas");
|
|
1095
|
+
canvas.width = cw; canvas.height = ch;
|
|
1096
|
+
var g = canvas.getContext("2d");
|
|
1097
|
+
g.fillStyle = "#ffffff";
|
|
1098
|
+
g.fillRect(0, 0, cw, ch);
|
|
1099
|
+
g.drawImage(img, 0, 0, cw, ch);
|
|
1100
|
+
var uri = canvas.toDataURL("image/jpeg", 0.92);
|
|
1101
|
+
var comma = uri.indexOf(",");
|
|
1102
|
+
if (comma < 0) return null;
|
|
1103
|
+
var binary = atob(uri.slice(comma + 1));
|
|
1104
|
+
var bytes = new Uint8Array(binary.length);
|
|
1105
|
+
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1106
|
+
return { bytes: bytes, width: cw, height: ch };
|
|
1107
|
+
} catch (e) {
|
|
1108
|
+
// A cross-origin image taints the canvas. Skipping it is correct:
|
|
1109
|
+
// there is no way to read its pixels, and failing the whole export
|
|
1110
|
+
// over one decorative image would be worse.
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ------------------------------------------------------------- downloads
|
|
1116
|
+
function sanitizeName(name) {
|
|
1117
|
+
if (!name || typeof name !== "string") return "";
|
|
1118
|
+
return name.replace(/\.pdf$/i, "").replace(/[\\/:*?"<>|]+/g, "").trim().slice(0, 120);
|
|
1119
|
+
}
|
|
1120
|
+
function defaultBase() {
|
|
1121
|
+
var d = new Date();
|
|
1122
|
+
function two(n) { return (n < 10 ? "0" : "") + n; }
|
|
1123
|
+
return "Export-" + String(d.getFullYear()).slice(2) + two(d.getMonth() + 1) + two(d.getDate()) +
|
|
1124
|
+
"-" + two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds());
|
|
1125
|
+
}
|
|
1126
|
+
function download(bytes, filename) {
|
|
1127
|
+
var blob = new Blob([bytes], { type: "application/pdf" });
|
|
1128
|
+
var url = URL.createObjectURL(blob);
|
|
1129
|
+
var a = document.createElement("a");
|
|
1130
|
+
a.href = url; a.download = filename;
|
|
1131
|
+
document.body.appendChild(a);
|
|
1132
|
+
a.click();
|
|
1133
|
+
document.body.removeChild(a);
|
|
1134
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
1135
|
+
return filename;
|
|
1136
|
+
}
|
|
1137
|
+
}
|