@richscripts/richtexteditor 2.14.1 → 2.16.0
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/README.md +182 -10
- package/angular/build-manifest.json +23 -0
- package/angular/index.d.ts +71 -76
- package/angular/index.mjs +2 -0
- package/angular/package.json +4 -0
- package/headless/index.d.ts +3 -1
- package/headless/index.js +72 -16
- package/index.d.ts +40 -52
- package/integrations/initialization-scope.js +27 -0
- package/integrations/native-mount.js +30 -0
- package/integrations/shared.d.ts +23 -1
- package/integrations/shared.js +68 -22
- package/package.json +25 -3
- package/react/index.js +118 -37
- package/react/index.mjs +29 -0
- package/richtexteditor/all_plugins.js +8816 -2373
- package/richtexteditor/all_plugins.min.js +77 -59
- package/richtexteditor/lang/emoji/LICENSE-CLDR.txt +41 -0
- package/richtexteditor/lang/emoji/rte-emoji-de.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-es.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-fr.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-it.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ja.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ko.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-nl.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-pt-pt.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-pt.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-ru.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-zh-Hans.js +3 -0
- package/richtexteditor/lang/emoji/rte-emoji-zh-Hant.js +3 -0
- package/richtexteditor/lang/rte-lang-af.js +1 -0
- package/richtexteditor/lang/rte-lang-ar.js +1 -0
- package/richtexteditor/lang/rte-lang-bg.js +1 -0
- package/richtexteditor/lang/rte-lang-bn.js +1 -0
- package/richtexteditor/lang/rte-lang-bs.js +1 -0
- package/richtexteditor/lang/rte-lang-ca.js +1 -0
- package/richtexteditor/lang/rte-lang-cs.js +1 -0
- package/richtexteditor/lang/rte-lang-cy.js +1 -0
- package/richtexteditor/lang/rte-lang-da.js +1 -0
- package/richtexteditor/lang/rte-lang-de.js +1050 -2
- package/richtexteditor/lang/rte-lang-el.js +1 -0
- package/richtexteditor/lang/rte-lang-en.js +1 -0
- package/richtexteditor/lang/rte-lang-es.js +1056 -2
- package/richtexteditor/lang/rte-lang-et.js +1 -0
- package/richtexteditor/lang/rte-lang-fa.js +1 -0
- package/richtexteditor/lang/rte-lang-fi.js +1 -0
- package/richtexteditor/lang/rte-lang-fil.js +1 -0
- package/richtexteditor/lang/rte-lang-fj.js +1 -0
- package/richtexteditor/lang/rte-lang-fr.js +1056 -2
- package/richtexteditor/lang/rte-lang-ga.js +1 -0
- package/richtexteditor/lang/rte-lang-gu.js +1 -0
- package/richtexteditor/lang/rte-lang-he.js +1 -0
- package/richtexteditor/lang/rte-lang-hi.js +1 -0
- package/richtexteditor/lang/rte-lang-hr.js +1 -0
- package/richtexteditor/lang/rte-lang-ht.js +1 -0
- package/richtexteditor/lang/rte-lang-hu.js +1 -0
- package/richtexteditor/lang/rte-lang-id.js +1 -0
- package/richtexteditor/lang/rte-lang-is.js +1 -0
- package/richtexteditor/lang/rte-lang-it.js +1056 -2
- package/richtexteditor/lang/rte-lang-ja.js +1056 -2
- package/richtexteditor/lang/rte-lang-kn.js +1 -0
- package/richtexteditor/lang/rte-lang-ko.js +1056 -2
- package/richtexteditor/lang/rte-lang-lt.js +1 -0
- package/richtexteditor/lang/rte-lang-lv.js +1 -0
- package/richtexteditor/lang/rte-lang-mg.js +1 -0
- package/richtexteditor/lang/rte-lang-mi.js +1 -0
- package/richtexteditor/lang/rte-lang-ml.js +1 -0
- package/richtexteditor/lang/rte-lang-mr.js +1 -0
- package/richtexteditor/lang/rte-lang-ms.js +1 -0
- package/richtexteditor/lang/rte-lang-mt.js +1 -0
- package/richtexteditor/lang/rte-lang-mww.js +1 -0
- package/richtexteditor/lang/rte-lang-nb.js +1 -0
- package/richtexteditor/lang/rte-lang-nl.js +1056 -2
- package/richtexteditor/lang/rte-lang-otq.js +1 -0
- package/richtexteditor/lang/rte-lang-pa.js +1 -0
- package/richtexteditor/lang/rte-lang-pl.js +1 -0
- package/richtexteditor/lang/rte-lang-pt-pt.js +1056 -2
- package/richtexteditor/lang/rte-lang-pt.js +1056 -2
- package/richtexteditor/lang/rte-lang-ro.js +1 -0
- package/richtexteditor/lang/rte-lang-ru.js +1056 -2
- package/richtexteditor/lang/rte-lang-sk.js +1 -0
- package/richtexteditor/lang/rte-lang-sl.js +1 -0
- package/richtexteditor/lang/rte-lang-sm.js +1 -0
- package/richtexteditor/lang/rte-lang-sr-Cyrl.js +1 -0
- package/richtexteditor/lang/rte-lang-sr-Latn.js +1 -0
- package/richtexteditor/lang/rte-lang-sv.js +1 -0
- package/richtexteditor/lang/rte-lang-sw.js +1 -0
- package/richtexteditor/lang/rte-lang-ta.js +1 -0
- package/richtexteditor/lang/rte-lang-te.js +1 -0
- package/richtexteditor/lang/rte-lang-th.js +1 -0
- package/richtexteditor/lang/rte-lang-tlh-Latn.js +1 -0
- package/richtexteditor/lang/rte-lang-tlh-Piqd.js +1 -0
- package/richtexteditor/lang/rte-lang-to.js +1 -0
- package/richtexteditor/lang/rte-lang-tr.js +1 -0
- package/richtexteditor/lang/rte-lang-ty.js +1 -0
- package/richtexteditor/lang/rte-lang-uk.js +1 -0
- package/richtexteditor/lang/rte-lang-ur.js +1 -0
- package/richtexteditor/lang/rte-lang-vi.js +1 -0
- package/richtexteditor/lang/rte-lang-yua.js +1 -0
- package/richtexteditor/lang/rte-lang-zh-Hans.js +1050 -2
- package/richtexteditor/lang/rte-lang-zh-Hant.js +1050 -2
- package/richtexteditor/plugins/a11yenhance.js +4 -1
- package/richtexteditor/plugins/a11yenhance.min.js +1 -1
- package/richtexteditor/plugins/accessibilitychecker.js +256 -64
- package/richtexteditor/plugins/accessibilitychecker.min.js +1 -1
- package/richtexteditor/plugins/aitoolkit.js +1057 -386
- package/richtexteditor/plugins/aitoolkit.min.js +7 -7
- package/richtexteditor/plugins/all_plugins.js +8816 -2373
- package/richtexteditor/plugins/all_plugins.min.js +77 -59
- package/richtexteditor/plugins/autocorrect.js +66 -31
- package/richtexteditor/plugins/autocorrect.min.js +1 -1
- package/richtexteditor/plugins/autoembed.js +18 -1
- package/richtexteditor/plugins/autoembed.min.js +1 -1
- package/richtexteditor/plugins/blocktypes.js +18 -9
- package/richtexteditor/plugins/blocktypes.min.js +1 -1
- package/richtexteditor/plugins/bookmarkcard.js +12 -5
- package/richtexteditor/plugins/bookmarkcard.min.js +2 -2
- package/richtexteditor/plugins/changecase.js +72 -19
- package/richtexteditor/plugins/changecase.min.js +1 -1
- package/richtexteditor/plugins/charlimit.js +62 -2
- package/richtexteditor/plugins/charlimit.min.js +2 -2
- package/richtexteditor/plugins/comments.js +460 -61
- package/richtexteditor/plugins/comments.min.js +4 -3
- package/richtexteditor/plugins/contentminimap.js +28 -9
- package/richtexteditor/plugins/contentminimap.min.js +1 -1
- package/richtexteditor/plugins/crdt-engine.js +27 -1
- package/richtexteditor/plugins/crossreference.js +1 -1
- package/richtexteditor/plugins/crossreference.min.js +1 -1
- package/richtexteditor/plugins/dialoga11y.js +26 -4
- package/richtexteditor/plugins/dialoga11y.min.js +1 -1
- package/richtexteditor/plugins/dictation.js +1 -1
- package/richtexteditor/plugins/documentimport.js +250 -59
- package/richtexteditor/plugins/documentimport.min.js +4 -4
- package/richtexteditor/plugins/documentoutline.js +47 -8
- package/richtexteditor/plugins/documentoutline.min.js +1 -1
- package/richtexteditor/plugins/docxexport.js +233 -32
- package/richtexteditor/plugins/docxexport.min.js +2 -2
- package/richtexteditor/plugins/draghandle.js +267 -3
- package/richtexteditor/plugins/draghandle.min.js +1 -1
- package/richtexteditor/plugins/emailtoolkit.js +28 -14
- package/richtexteditor/plugins/emailtoolkit.min.js +1 -1
- package/richtexteditor/plugins/emojiautocomplete.js +111 -57
- package/richtexteditor/plugins/emojiautocomplete.min.js +1 -1
- package/richtexteditor/plugins/footnotes.js +65 -11
- package/richtexteditor/plugins/footnotes.min.js +1 -1
- package/richtexteditor/plugins/formatpainter.js +10 -0
- package/richtexteditor/plugins/formatpainter.min.js +1 -1
- package/richtexteditor/plugins/formattingmarks.js +6 -6
- package/richtexteditor/plugins/formattingmarks.min.js +1 -1
- package/richtexteditor/plugins/gapcursor.js +60 -0
- package/richtexteditor/plugins/gapcursor.min.js +1 -1
- package/richtexteditor/plugins/ghostcomplete.js +1 -1
- package/richtexteditor/plugins/html2pdf.js +8 -8
- package/richtexteditor/plugins/html2pdf.min.js +1 -1
- package/richtexteditor/plugins/imageeditor.js +31 -12
- package/richtexteditor/plugins/imageeditor.min.js +1 -1
- package/richtexteditor/plugins/importstyles.js +1 -1
- package/richtexteditor/plugins/inlinecode.js +25 -0
- package/richtexteditor/plugins/inlinecode.min.js +1 -1
- package/richtexteditor/plugins/insertcode.js +29 -11
- package/richtexteditor/plugins/insertcode.min.js +6 -6
- package/richtexteditor/plugins/insertemoji.js +113 -16
- package/richtexteditor/plugins/insertemoji.min.js +1 -1
- package/richtexteditor/plugins/insertgallery.js +46 -40
- package/richtexteditor/plugins/insertgallery.min.js +1 -1
- package/richtexteditor/plugins/inserttemplate.js +43 -19
- package/richtexteditor/plugins/inserttemplate.min.js +1 -1
- package/richtexteditor/plugins/keyboarda11y.js +66 -3
- package/richtexteditor/plugins/keyboarda11y.min.js +1 -1
- package/richtexteditor/plugins/layouttable.js +20 -0
- package/richtexteditor/plugins/layouttable.min.js +1 -1
- package/richtexteditor/plugins/linkchecker.js +29 -9
- package/richtexteditor/plugins/linkchecker.min.js +1 -1
- package/richtexteditor/plugins/localdraft.js +51 -7
- package/richtexteditor/plugins/localdraft.min.js +1 -1
- package/richtexteditor/plugins/markdownexport.js +159 -0
- package/richtexteditor/plugins/markdownexport.min.js +2 -2
- package/richtexteditor/plugins/mathdialog.js +11 -5
- package/richtexteditor/plugins/mathdialog.min.js +1 -1
- package/richtexteditor/plugins/mentionplugin.js +90 -27
- package/richtexteditor/plugins/mentionplugin.min.js +1 -1
- package/richtexteditor/plugins/mermaiddiagram.js +14 -8
- package/richtexteditor/plugins/mermaiddiagram.min.js +2 -2
- package/richtexteditor/plugins/multilevellist.js +4 -0
- package/richtexteditor/plugins/multilevellist.min.js +1 -1
- package/richtexteditor/plugins/nodetypes.js +1 -1
- package/richtexteditor/plugins/nodetypes.min.js +1 -1
- package/richtexteditor/plugins/pagination.js +2 -2
- package/richtexteditor/plugins/pagination.min.js +1 -1
- package/richtexteditor/plugins/pdfexport.js +1752 -1167
- package/richtexteditor/plugins/pdfexport.min.js +28 -13
- package/richtexteditor/plugins/permanentpen.js +3 -3
- package/richtexteditor/plugins/permanentpen.min.js +1 -1
- package/richtexteditor/plugins/readabilitystats.js +25 -11
- package/richtexteditor/plugins/readabilitystats.min.js +1 -1
- package/richtexteditor/plugins/readaloud.js +30 -16
- package/richtexteditor/plugins/readaloud.min.js +1 -1
- package/richtexteditor/plugins/restrictedediting.js +166 -0
- package/richtexteditor/plugins/restrictedediting.min.js +2 -2
- package/richtexteditor/plugins/revisionhistory.js +173 -41
- package/richtexteditor/plugins/revisionhistory.min.js +3 -3
- package/richtexteditor/plugins/rubyannotation.js +21 -11
- package/richtexteditor/plugins/rubyannotation.min.js +1 -1
- package/richtexteditor/plugins/sanitizer.js +119 -7
- package/richtexteditor/plugins/sanitizer.min.js +1 -1
- package/richtexteditor/plugins/slashcommand.js +32 -18
- package/richtexteditor/plugins/slashcommand.min.js +2 -2
- package/richtexteditor/plugins/smartchips.js +39 -25
- package/richtexteditor/plugins/smartchips.min.js +2 -2
- package/richtexteditor/plugins/spellcheck.js +20 -11
- package/richtexteditor/plugins/spellcheck.min.js +2 -2
- package/richtexteditor/plugins/stateinspector.js +21 -11
- package/richtexteditor/plugins/stateinspector.min.js +2 -2
- package/richtexteditor/plugins/structured-content-bridge.js +90 -32
- package/richtexteditor/plugins/structured-content-bridge.min.js +8 -8
- package/richtexteditor/plugins/tableofcontents.js +5 -2
- package/richtexteditor/plugins/tableofcontents.min.js +1 -1
- package/richtexteditor/plugins/tabletools.js +388 -3
- package/richtexteditor/plugins/tabletools.min.js +1 -1
- package/richtexteditor/plugins/textdirection.js +19 -6
- package/richtexteditor/plugins/textdirection.min.js +1 -1
- package/richtexteditor/plugins/textpartlanguage.js +161 -32
- package/richtexteditor/plugins/textpartlanguage.min.js +1 -1
- package/richtexteditor/plugins/todolist.js +90 -6
- package/richtexteditor/plugins/todolist.min.js +1 -1
- package/richtexteditor/plugins/trackedchanges.js +1812 -25
- package/richtexteditor/plugins/trackedchanges.min.js +5 -3
- package/richtexteditor/plugins/wordexport.js +2 -2
- package/richtexteditor/plugins/yjscollab.js +11 -6
- package/richtexteditor/plugins/yjscollab.min.js +1 -1
- package/richtexteditor/plugins/zz_richtextbox_dialog_style.js +22 -13
- package/richtexteditor/plugins/zz_richtextbox_dialog_style.min.js +1 -1
- package/richtexteditor/rte-config.js +144 -86
- package/richtexteditor/rte-headless.js +19 -4
- package/richtexteditor/rte-watchdog.js +3 -1
- package/richtexteditor/rte-webcomponent.js +91 -16
- package/richtexteditor/rte.js +146 -88
- package/richtexteditor/runtime/colorpicker_more_ns.htm +3 -1
- package/scripts/build-angular.mjs +97 -0
- package/svelte/index.d.ts +2 -1
- package/svelte/index.js +80 -9
- package/vue/index.js +130 -34
- package/angular/component.ts +0 -297
- package/angular/index.ts +0 -24
|
@@ -1,1167 +1,1752 @@
|
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
var
|
|
140
|
-
var
|
|
141
|
-
var
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
var
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
function
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
var
|
|
222
|
-
for (var
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
var
|
|
315
|
-
var
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
function
|
|
383
|
-
var
|
|
384
|
-
var
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
function
|
|
401
|
-
|
|
402
|
-
var
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
var
|
|
429
|
-
|
|
430
|
-
var
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
var
|
|
534
|
-
var
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
var
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
var
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
var
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
var
|
|
722
|
-
var
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
for (var
|
|
862
|
-
var
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
var
|
|
879
|
-
for (var
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
this.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
var
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
for (var i = 0; i <
|
|
1025
|
-
var
|
|
1026
|
-
if (
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
var
|
|
1152
|
-
function
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
// An ordered list item's number in its list style: 1, a..z then aa (CSS lower-alpha), i/I roman.
|
|
40
|
+
function listNumber(n, kind) {
|
|
41
|
+
if (kind === "a" || kind === "A") {
|
|
42
|
+
var t = "";
|
|
43
|
+
for (var k = n; k > 0; k = Math.floor((k - 1) / 26)) t = String.fromCharCode(97 + (k - 1) % 26) + t;
|
|
44
|
+
return t ? (kind === "A" ? t.toUpperCase() : t) : String(n); // 0 and below: a number, never ""
|
|
45
|
+
}
|
|
46
|
+
if ((kind === "i" || kind === "I") && n > 0 && n < 4000) {
|
|
47
|
+
var r = "", vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], sy = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
|
|
48
|
+
for (var q = 0; q < vals.length; q++) while (n >= vals[q]) { r += sy[q]; n -= vals[q]; }
|
|
49
|
+
return kind === "I" ? r.toUpperCase() : r;
|
|
50
|
+
}
|
|
51
|
+
return String(n);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var obj = this;
|
|
55
|
+
var config, editor;
|
|
56
|
+
|
|
57
|
+
obj.PluginName = "PdfExport";
|
|
58
|
+
|
|
59
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
60
|
+
|
|
61
|
+
obj.InitEditor = function (argeditor) {
|
|
62
|
+
editor = argeditor;
|
|
63
|
+
if (config.pdfExport === false) return;
|
|
64
|
+
|
|
65
|
+
editor.getPdfBytes = function (options) { return buildPdf(options || {}); };
|
|
66
|
+
// 2026-10-26 The standard PDF fonts encode Western European Latin only (WinAnsi): every other character -
|
|
67
|
+
// Chinese, Japanese, Korean, Cyrillic, Greek, Arabic... - was written as "?", silently. Until fonts can be
|
|
68
|
+
// embedded: getPdfUnsupportedText() says which characters a vector export would lose, and exportToPdf() (the
|
|
69
|
+
// toolbar's Export PDF) hands such a document to the html2pdf raster export when that plugin is loaded, which
|
|
70
|
+
// draws every script. Set pdfExportRasterFallback = false to always write the vector PDF.
|
|
71
|
+
// tools/pdf/pdf-nonlatin-fallback.mjs
|
|
72
|
+
editor.getPdfUnsupportedText = function (options) { return unsupportedText(options || {}); };
|
|
73
|
+
// Fetches a TrueType font from a URL you choose and uses it for the text the standard fonts cannot draw.
|
|
74
|
+
// Resolves true when it is usable; nothing is fetched unless this is called. tools/pdf/pdf-unicode-font.mjs
|
|
75
|
+
editor.loadPdfFont = function (url) {
|
|
76
|
+
return fetch(url).then(function (r) { if (!r.ok) throw new Error("pdfexport: font " + url + " returned " + r.status); return r.arrayBuffer(); })
|
|
77
|
+
.then(function (buf) { var bytes = new Uint8Array(buf); parseTrueType(bytes); config.pdfExportUnicodeFont = bytes; return true; });
|
|
78
|
+
};
|
|
79
|
+
editor.exportToPdf = function (filename, options) {
|
|
80
|
+
var lost = unsupportedText(options || {});
|
|
81
|
+
if (lost) {
|
|
82
|
+
var raster = config.pdfExportRasterFallback !== false && editor.toolbarFactoryMap && editor.toolbarFactoryMap["html2pdf"];
|
|
83
|
+
if (window.console && console.warn) {
|
|
84
|
+
console.warn("pdfexport: the vector PDF cannot encode " + lost.length + " character(s) of this document (" +
|
|
85
|
+
lost.slice(0, 20) + (lost.length > 20 ? "..." : "") + "); its standard fonts cover Western European Latin only. " +
|
|
86
|
+
(raster ? "Exporting with the html2pdf raster export instead." : "They are written as \"?\". Load the html2pdf plugin for a raster export that keeps them."));
|
|
87
|
+
}
|
|
88
|
+
if (raster) { editor.execCommand("html2pdf"); return null; }
|
|
89
|
+
}
|
|
90
|
+
var bytes = buildPdf(options || {});
|
|
91
|
+
var base = sanitizeName(filename) || sanitizeName(config.pdfExportFileName) || defaultBase();
|
|
92
|
+
return download(bytes, base + ".pdf");
|
|
93
|
+
};
|
|
94
|
+
editor.downloadPdf = editor.exportToPdf;
|
|
95
|
+
|
|
96
|
+
editor.attachEvent("exec_command_exportpdf", function (state) {
|
|
97
|
+
state.returnValue = true;
|
|
98
|
+
state.stopBubble = true;
|
|
99
|
+
try { editor.exportToPdf(); }
|
|
100
|
+
catch (e) { if (window.console) console.error("pdfexport:", e); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// The slash-menu entry lives in slashcommand.js, gated on
|
|
104
|
+
// `typeof editor.exportToPdf === "function"`. Registering from here
|
|
105
|
+
// instead would silently do nothing: plugins initialise in bundle
|
|
106
|
+
// order, "pdfexport" sorts before "slashcommand", and
|
|
107
|
+
// editor.slashCommands does not exist yet at this point.
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------- units
|
|
111
|
+
var PT_PER_IN = 72;
|
|
112
|
+
var PAGE_SIZES = { // points, portrait
|
|
113
|
+
letter: [612, 792], legal: [612, 1008], tabloid: [792, 1224],
|
|
114
|
+
a3: [842, 1191], a4: [595, 842], a5: [420, 595]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function toPoints(v, fallback) {
|
|
118
|
+
if (typeof v === "number" && isFinite(v)) return v * PT_PER_IN; // bare number = inches
|
|
119
|
+
if (typeof v !== "string") return fallback;
|
|
120
|
+
var m = /^\s*(-?[\d.]+)\s*(mm|cm|in|px|pt)?\s*$/.exec(v);
|
|
121
|
+
if (!m) return fallback;
|
|
122
|
+
var n = parseFloat(m[1]);
|
|
123
|
+
if (!isFinite(n)) return fallback;
|
|
124
|
+
switch (m[2]) {
|
|
125
|
+
case "mm": return n * PT_PER_IN / 25.4;
|
|
126
|
+
case "cm": return n * PT_PER_IN / 2.54;
|
|
127
|
+
case "pt": return n;
|
|
128
|
+
case "px": return n * 0.75; // CSS px at 96dpi
|
|
129
|
+
default: return n * PT_PER_IN;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pageGeometry(options) {
|
|
134
|
+
var setup = null;
|
|
135
|
+
try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); }
|
|
136
|
+
catch (e) { setup = null; }
|
|
137
|
+
setup = setup || {};
|
|
138
|
+
var fmt = String(options.format || setup.format || "letter").toLowerCase();
|
|
139
|
+
var size = PAGE_SIZES[fmt] || PAGE_SIZES.letter;
|
|
140
|
+
var landscape = String(options.orientation || setup.orientation || "").toLowerCase() === "landscape";
|
|
141
|
+
var w = landscape ? size[1] : size[0];
|
|
142
|
+
var h = landscape ? size[0] : size[1];
|
|
143
|
+
var m = options.margins || setup.margins || {};
|
|
144
|
+
var half = PT_PER_IN / 2;
|
|
145
|
+
return {
|
|
146
|
+
width: w, height: h,
|
|
147
|
+
margin: {
|
|
148
|
+
top: toPoints(m.top, half), right: toPoints(m.right, half),
|
|
149
|
+
bottom: toPoints(m.bottom, half), left: toPoints(m.left, half)
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------- fonts
|
|
155
|
+
//
|
|
156
|
+
// The base-14 fonts need no embedding, but a viewer would then lay text out
|
|
157
|
+
// with ITS metrics while this code wrapped lines using its own. The two
|
|
158
|
+
// disagree and text overruns the margin. Fixing it by shipping AFM width
|
|
159
|
+
// tables would add ~1500 lines of data; instead the widths are measured here
|
|
160
|
+
// and written into the font object as /Widths, which a viewer is required to
|
|
161
|
+
// honour. Layout and rendering then use the same numbers by construction.
|
|
162
|
+
//
|
|
163
|
+
// Measurement is accurate because the base-14 fonts are metric-compatible
|
|
164
|
+
// with fonts every browser has: Helvetica/Arial, Times/Times New Roman,
|
|
165
|
+
// Courier/Courier New were designed to share advance widths.
|
|
166
|
+
var FONT_FAMILIES = {
|
|
167
|
+
helvetica: { css: "Arial, Helvetica, sans-serif", faces: ["Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique"] },
|
|
168
|
+
times: { css: '"Times New Roman", Times, serif', faces: ["Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic"] },
|
|
169
|
+
courier: { css: '"Courier New", Courier, monospace', faces: ["Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"] }
|
|
170
|
+
};
|
|
171
|
+
var FIRST_CHAR = 32, LAST_CHAR = 255;
|
|
172
|
+
|
|
173
|
+
var widthCache = {};
|
|
174
|
+
function widthsFor(familyKey, styleIndex) {
|
|
175
|
+
var key = familyKey + ":" + styleIndex;
|
|
176
|
+
if (widthCache[key]) return widthCache[key];
|
|
177
|
+
var fam = FONT_FAMILIES[familyKey] || FONT_FAMILIES.helvetica;
|
|
178
|
+
var canvas = widthsFor.canvas || (widthsFor.canvas = document.createElement("canvas"));
|
|
179
|
+
var g = canvas.getContext("2d");
|
|
180
|
+
// 1000 units per em is the PDF glyph space; measuring at 100px and
|
|
181
|
+
// scaling by 10 keeps sub-unit rounding away from the result.
|
|
182
|
+
var weight = (styleIndex & 1) ? "bold " : "";
|
|
183
|
+
var slant = (styleIndex & 2) ? "italic " : "";
|
|
184
|
+
g.font = weight + slant + "100px " + fam.css;
|
|
185
|
+
var out = [];
|
|
186
|
+
for (var c = FIRST_CHAR; c <= LAST_CHAR; c++) {
|
|
187
|
+
var w = g.measureText(String.fromCharCode(c)).width;
|
|
188
|
+
out.push(Math.round(w * 10));
|
|
189
|
+
}
|
|
190
|
+
widthCache[key] = out;
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function charWidth(familyKey, styleIndex, code) {
|
|
195
|
+
if (code < FIRST_CHAR || code > LAST_CHAR) code = 63; // '?'
|
|
196
|
+
return widthsFor(familyKey, styleIndex)[code - FIRST_CHAR] || 500;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function textWidth(str, familyKey, styleIndex, size) {
|
|
200
|
+
var total = 0;
|
|
201
|
+
for (var i = 0; i < str.length; i++) total += charWidth(familyKey, styleIndex, str.charCodeAt(i));
|
|
202
|
+
return total * size / 1000;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ------------------------------------------------------------ embedded Unicode font
|
|
206
|
+
//
|
|
207
|
+
// 2026-10-26 The base-14 fonts encode Western European Latin only. With a TrueType font supplied
|
|
208
|
+
// (config.pdfExportUnicodeFont / options.unicodeFont: the bytes of a .ttf, or editor.loadPdfFont(url)),
|
|
209
|
+
// text the base fonts cannot draw - Chinese, Japanese, Korean, Cyrillic, Greek... - is written in that font
|
|
210
|
+
// instead: a Type0 / CIDFontType2 font with Identity-H encoding and a ToUnicode map, so it stays vector,
|
|
211
|
+
// selectable and searchable. The font is subset: every glyph keeps its ID (so CIDToGIDMap stays Identity)
|
|
212
|
+
// but only the outlines the document uses are kept. tools/pdf/pdf-unicode-font.mjs
|
|
213
|
+
var activeUni = null; // { font, used: { gid: codepoint } } while one export runs
|
|
214
|
+
var parsedFonts = []; // [{ bytes, font }] - a parse is reused for the same bytes
|
|
215
|
+
|
|
216
|
+
function u16(b, o) { return (b[o] << 8) | b[o + 1]; }
|
|
217
|
+
function s16(b, o) { var v = u16(b, o); return v & 0x8000 ? v - 0x10000 : v; }
|
|
218
|
+
function u32(b, o) { return ((b[o] << 24) >>> 0) + (b[o + 1] << 16) + (b[o + 2] << 8) + b[o + 3]; }
|
|
219
|
+
|
|
220
|
+
function parseTrueType(input) {
|
|
221
|
+
var b = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
222
|
+
for (var pi = 0; pi < parsedFonts.length; pi++) if (parsedFonts[pi].bytes === b) return parsedFonts[pi].font;
|
|
223
|
+
var tag0 = u32(b, 0);
|
|
224
|
+
if (tag0 !== 0x00010000 && tag0 !== 0x74727565) {
|
|
225
|
+
throw new Error(tag0 === 0x4F54544F ? "pdfexport: the Unicode font has CFF (PostScript) outlines; supply a TrueType (.ttf) font"
|
|
226
|
+
: tag0 === 0x74746366 ? "pdfexport: the Unicode font is a collection (.ttc); supply a single .ttf font"
|
|
227
|
+
: "pdfexport: the Unicode font is not a TrueType font");
|
|
228
|
+
}
|
|
229
|
+
var tables = {}, n = u16(b, 4);
|
|
230
|
+
for (var i = 0; i < n; i++) {
|
|
231
|
+
var rec = 12 + i * 16;
|
|
232
|
+
var tag = String.fromCharCode(b[rec], b[rec + 1], b[rec + 2], b[rec + 3]);
|
|
233
|
+
tables[tag] = { offset: u32(b, rec + 8), length: u32(b, rec + 12) };
|
|
234
|
+
}
|
|
235
|
+
["head", "hhea", "hmtx", "maxp", "cmap", "loca", "glyf"].forEach(function (t) {
|
|
236
|
+
if (!tables[t]) throw new Error("pdfexport: the Unicode font has no '" + t + "' table");
|
|
237
|
+
});
|
|
238
|
+
var head = tables.head.offset, hhea = tables.hhea.offset;
|
|
239
|
+
var font = {
|
|
240
|
+
bytes: b, tables: tables,
|
|
241
|
+
units: u16(b, head + 18),
|
|
242
|
+
bbox: [s16(b, head + 36), s16(b, head + 38), s16(b, head + 40), s16(b, head + 42)],
|
|
243
|
+
longLoca: s16(b, head + 50) === 1,
|
|
244
|
+
numGlyphs: u16(b, tables.maxp.offset + 4),
|
|
245
|
+
numHMetrics: u16(b, hhea + 34),
|
|
246
|
+
ascent: s16(b, hhea + 4), descent: s16(b, hhea + 6), capHeight: 0, italicAngle: 0
|
|
247
|
+
};
|
|
248
|
+
if (tables["OS/2"]) {
|
|
249
|
+
var os2 = tables["OS/2"].offset;
|
|
250
|
+
font.ascent = s16(b, os2 + 68); font.descent = s16(b, os2 + 70);
|
|
251
|
+
if (u16(b, os2) >= 2 && tables["OS/2"].length >= 90) font.capHeight = s16(b, os2 + 88);
|
|
252
|
+
}
|
|
253
|
+
if (!font.capHeight) font.capHeight = font.ascent;
|
|
254
|
+
if (tables.post) font.italicAngle = s16(b, tables.post.offset + 4);
|
|
255
|
+
// cmap: a Unicode subtable, format 12 (full range) preferred over format 4 (BMP)
|
|
256
|
+
var cmap = tables.cmap.offset, best = null, bestFmt = 0;
|
|
257
|
+
for (var c = 0, nc = u16(b, cmap + 2); c < nc; c++) {
|
|
258
|
+
var pid = u16(b, cmap + 4 + c * 8), eid = u16(b, cmap + 6 + c * 8), off = cmap + u32(b, cmap + 8 + c * 8);
|
|
259
|
+
var fmt = u16(b, off);
|
|
260
|
+
var unicode = pid === 0 || (pid === 3 && (eid === 1 || eid === 10));
|
|
261
|
+
if (!unicode || (fmt !== 4 && fmt !== 12)) continue;
|
|
262
|
+
if (fmt > bestFmt) { best = off; bestFmt = fmt; }
|
|
263
|
+
}
|
|
264
|
+
if (!best) throw new Error("pdfexport: the Unicode font has no Unicode character map");
|
|
265
|
+
var lookup = {};
|
|
266
|
+
font.gidFor = function (cp) {
|
|
267
|
+
if (lookup[cp] !== undefined) return lookup[cp];
|
|
268
|
+
var g = 0;
|
|
269
|
+
if (bestFmt === 12) {
|
|
270
|
+
var groups = u32(b, best + 12);
|
|
271
|
+
for (var k = 0; k < groups; k++) {
|
|
272
|
+
var go = best + 16 + k * 12, start = u32(b, go), end = u32(b, go + 4);
|
|
273
|
+
if (cp >= start && cp <= end) { g = u32(b, go + 8) + (cp - start); break; }
|
|
274
|
+
}
|
|
275
|
+
} else if (cp <= 0xFFFF) {
|
|
276
|
+
var segX2 = u16(b, best + 6), ends = best + 14, starts = ends + segX2 + 2, deltas = starts + segX2, ranges = deltas + segX2;
|
|
277
|
+
for (var s = 0; s < segX2 / 2; s++) {
|
|
278
|
+
if (cp > u16(b, ends + s * 2)) continue;
|
|
279
|
+
var st = u16(b, starts + s * 2);
|
|
280
|
+
if (cp < st) break;
|
|
281
|
+
var ro = u16(b, ranges + s * 2);
|
|
282
|
+
if (!ro) g = (cp + s16(b, deltas + s * 2)) & 0xFFFF;
|
|
283
|
+
else {
|
|
284
|
+
var gi = u16(b, ranges + s * 2 + ro + (cp - st) * 2);
|
|
285
|
+
g = gi ? (gi + s16(b, deltas + s * 2)) & 0xFFFF : 0;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (g >= font.numGlyphs) g = 0;
|
|
291
|
+
lookup[cp] = g;
|
|
292
|
+
return g;
|
|
293
|
+
};
|
|
294
|
+
font.advance = function (gid) {
|
|
295
|
+
var hm = font.tables.hmtx.offset;
|
|
296
|
+
var i2 = gid < font.numHMetrics ? gid : font.numHMetrics - 1;
|
|
297
|
+
return u16(b, hm + i2 * 4);
|
|
298
|
+
};
|
|
299
|
+
parsedFonts.push({ bytes: b, font: font });
|
|
300
|
+
if (parsedFonts.length > 4) parsedFonts.shift();
|
|
301
|
+
return font;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function glyphRange(font, gid) {
|
|
305
|
+
var b = font.bytes, loca = font.tables.loca.offset;
|
|
306
|
+
var a = font.longLoca ? u32(b, loca + gid * 4) : u16(b, loca + gid * 2) * 2;
|
|
307
|
+
var z = font.longLoca ? u32(b, loca + gid * 4 + 4) : u16(b, loca + gid * 2 + 2) * 2;
|
|
308
|
+
return [font.tables.glyf.offset + a, z - a];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A font with every glyph ID kept but only the used outlines (plus the components of composite glyphs and
|
|
312
|
+
// .notdef); tables a CIDFontType2 needs: head, hhea, hmtx, maxp, loca, glyf, and the hinting tables if present.
|
|
313
|
+
function subsetTrueType(font, usedGids) {
|
|
314
|
+
var b = font.bytes, keep = { 0: true }, stack = [];
|
|
315
|
+
for (var k in usedGids) { keep[k] = true; stack.push(+k); }
|
|
316
|
+
while (stack.length) {
|
|
317
|
+
var gid = stack.pop(), r = glyphRange(font, gid);
|
|
318
|
+
if (r[1] < 10 || s16(b, r[0]) >= 0) continue; // empty or simple glyph
|
|
319
|
+
var p = r[0] + 10, more = true;
|
|
320
|
+
while (more) {
|
|
321
|
+
var flags = u16(b, p), comp = u16(b, p + 2);
|
|
322
|
+
if (!keep[comp]) { keep[comp] = true; stack.push(comp); }
|
|
323
|
+
p += 4 + ((flags & 1) ? 4 : 2) + ((flags & 8) ? 2 : (flags & 0x40) ? 4 : (flags & 0x80) ? 8 : 0);
|
|
324
|
+
more = !!(flags & 0x20);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var glyfParts = [], locaOut = new Uint8Array((font.numGlyphs + 1) * 4), at = 0;
|
|
328
|
+
function put32(arr, o, v) { arr[o] = (v >>> 24) & 255; arr[o + 1] = (v >>> 16) & 255; arr[o + 2] = (v >>> 8) & 255; arr[o + 3] = v & 255; }
|
|
329
|
+
for (var g = 0; g < font.numGlyphs; g++) {
|
|
330
|
+
put32(locaOut, g * 4, at);
|
|
331
|
+
if (!keep[g]) continue;
|
|
332
|
+
var gr = glyphRange(font, g);
|
|
333
|
+
if (gr[1] <= 0) continue;
|
|
334
|
+
var len = (gr[1] + 3) & ~3;
|
|
335
|
+
var part = new Uint8Array(len); part.set(b.subarray(gr[0], gr[0] + gr[1]));
|
|
336
|
+
glyfParts.push(part); at += len;
|
|
337
|
+
}
|
|
338
|
+
put32(locaOut, font.numGlyphs * 4, at);
|
|
339
|
+
var glyfOut = new Uint8Array(at), q = 0;
|
|
340
|
+
for (var gp = 0; gp < glyfParts.length; gp++) { glyfOut.set(glyfParts[gp], q); q += glyfParts[gp].length; }
|
|
341
|
+
var out = {};
|
|
342
|
+
["head", "hhea", "hmtx", "maxp", "cvt ", "fpgm", "prep"].forEach(function (t) {
|
|
343
|
+
if (font.tables[t]) out[t] = b.slice(font.tables[t].offset, font.tables[t].offset + font.tables[t].length);
|
|
344
|
+
});
|
|
345
|
+
out.head[50] = 0; out.head[51] = 1; // indexToLocFormat: long
|
|
346
|
+
out.head[8] = out.head[9] = out.head[10] = out.head[11] = 0; // checkSumAdjustment, set below
|
|
347
|
+
out.loca = locaOut; out.glyf = glyfOut;
|
|
348
|
+
var tags = Object.keys(out).sort(), numT = tags.length, dirLen = 12 + numT * 16, total = dirLen;
|
|
349
|
+
tags.forEach(function (t) { total += (out[t].length + 3) & ~3; });
|
|
350
|
+
var f = new Uint8Array(total);
|
|
351
|
+
put32(f, 0, 0x00010000);
|
|
352
|
+
var es = Math.floor(Math.log(numT) / Math.LN2), sr = Math.pow(2, es) * 16;
|
|
353
|
+
f[4] = numT >> 8; f[5] = numT & 255; f[6] = sr >> 8; f[7] = sr & 255; f[8] = es >> 8; f[9] = es & 255;
|
|
354
|
+
var rs = numT * 16 - sr; f[10] = rs >> 8; f[11] = rs & 255;
|
|
355
|
+
function checksum(arr) { var sum = 0; for (var i = 0; i < arr.length; i += 4) sum = (sum + (((arr[i] << 24) >>> 0) + ((arr[i + 1] || 0) << 16) + ((arr[i + 2] || 0) << 8) + (arr[i + 3] || 0))) >>> 0; return sum; }
|
|
356
|
+
var pos = dirLen, headPos = 0;
|
|
357
|
+
tags.forEach(function (t, i) {
|
|
358
|
+
var rec = 12 + i * 16;
|
|
359
|
+
for (var c = 0; c < 4; c++) f[rec + c] = t.charCodeAt(c);
|
|
360
|
+
put32(f, rec + 4, checksum(out[t])); put32(f, rec + 8, pos); put32(f, rec + 12, out[t].length);
|
|
361
|
+
f.set(out[t], pos);
|
|
362
|
+
if (t === "head") headPos = pos;
|
|
363
|
+
pos += (out[t].length + 3) & ~3;
|
|
364
|
+
});
|
|
365
|
+
put32(f, headPos + 8, (0xB1B0AFBA - checksum(f)) >>> 0);
|
|
366
|
+
return f;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function hex4(n) { return ("000" + n.toString(16).toUpperCase()).slice(-4); }
|
|
370
|
+
// The glyph IDs of a string in the active Unicode font, as a PDF hex string; records each for the ToUnicode map.
|
|
371
|
+
function uniHex(str) {
|
|
372
|
+
var out = "";
|
|
373
|
+
for (var i = 0; i < str.length; i++) {
|
|
374
|
+
var cp = str.codePointAt(i);
|
|
375
|
+
if (cp > 0xFFFF) i++;
|
|
376
|
+
var gid = activeUni.font.gidFor(cp);
|
|
377
|
+
activeUni.used[gid] = cp;
|
|
378
|
+
out += hex4(gid);
|
|
379
|
+
}
|
|
380
|
+
return "<" + out + ">";
|
|
381
|
+
}
|
|
382
|
+
function uniWidth(str, size) {
|
|
383
|
+
var total = 0;
|
|
384
|
+
for (var i = 0; i < str.length; i++) {
|
|
385
|
+
var cp = str.codePointAt(i);
|
|
386
|
+
if (cp > 0xFFFF) i++;
|
|
387
|
+
total += activeUni.font.advance(activeUni.font.gidFor(cp));
|
|
388
|
+
}
|
|
389
|
+
return total * size / activeUni.font.units;
|
|
390
|
+
}
|
|
391
|
+
// Can the base fonts draw this character? (the same test toWinAnsi applies)
|
|
392
|
+
function winEncodable(ch) { return ch === "?" || toWinAnsi(ch).indexOf("?") < 0; }
|
|
393
|
+
// East Asian characters: a line may break between any two of them.
|
|
394
|
+
function breaksAnywhere(cp) {
|
|
395
|
+
return (cp >= 0x2E80 && cp <= 0x9FFF) || (cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
396
|
+
(cp >= 0xFF00 && cp <= 0xFFEF) || (cp >= 0x3000 && cp <= 0x303F) || (cp >= 0x20000 && cp <= 0x2FA1F);
|
|
397
|
+
}
|
|
398
|
+
// Splits a token into pieces drawn by the base fonts ({ text }) and by the Unicode font ({ text, uni: true }),
|
|
399
|
+
// with every East Asian character its own piece so a line can break there.
|
|
400
|
+
function uniPieces(tok) {
|
|
401
|
+
var pieces = [], cur = null;
|
|
402
|
+
for (var i = 0; i < tok.length; i++) {
|
|
403
|
+
var cp = tok.codePointAt(i), ch = String.fromCodePoint(cp);
|
|
404
|
+
if (cp > 0xFFFF) i++;
|
|
405
|
+
var uni = !winEncodable(ch) && activeUni.font.gidFor(cp) !== 0;
|
|
406
|
+
if (uni && breaksAnywhere(cp)) { cur = null; pieces.push({ text: ch, uni: true }); continue; }
|
|
407
|
+
if (!cur || cur.uni !== uni) { cur = { text: "", uni: uni }; pieces.push(cur); }
|
|
408
|
+
cur.text += ch;
|
|
409
|
+
}
|
|
410
|
+
return pieces;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ------------------------------------------------------------ PDF string
|
|
414
|
+
//
|
|
415
|
+
// WinAnsiEncoding covers Latin-1 plus the printable range of CP1252. A
|
|
416
|
+
// character outside it has no glyph in a base-14 font, so it is transliterated
|
|
417
|
+
// rather than written as a byte the viewer would render as garbage.
|
|
418
|
+
var WINANSI_EXTRA = {
|
|
419
|
+
0x20AC: 128, 0x201A: 130, 0x0192: 131, 0x201E: 132, 0x2026: 133, 0x2020: 134,
|
|
420
|
+
0x2021: 135, 0x02C6: 136, 0x2030: 137, 0x0160: 138, 0x2039: 139, 0x0152: 140,
|
|
421
|
+
0x017D: 142, 0x2018: 145, 0x2019: 146, 0x201C: 147, 0x201D: 148, 0x2022: 149,
|
|
422
|
+
0x2013: 150, 0x2014: 151, 0x02DC: 152, 0x2122: 153, 0x0161: 154, 0x203A: 155,
|
|
423
|
+
0x0153: 156, 0x017E: 158, 0x0178: 159
|
|
424
|
+
};
|
|
425
|
+
function toWinAnsi(str) {
|
|
426
|
+
var out = "";
|
|
427
|
+
for (var i = 0; i < str.length; i++) {
|
|
428
|
+
var cp = str.charCodeAt(i);
|
|
429
|
+
if (cp < 256) { out += String.fromCharCode(cp); continue; }
|
|
430
|
+
var mapped = WINANSI_EXTRA[cp];
|
|
431
|
+
if (mapped) { out += String.fromCharCode(mapped); continue; }
|
|
432
|
+
// Common shapes that would otherwise become "?" for no good reason.
|
|
433
|
+
if (cp === 0x00A0) { out += " "; continue; }
|
|
434
|
+
if (cp >= 0x2000 && cp <= 0x200A) { out += " "; continue; }
|
|
435
|
+
if (cp === 0x2212) { out += "-"; continue; }
|
|
436
|
+
// 2026-10-26 invisible format characters (zero-width space / joiners, word joiner, BOM) have no glyph to
|
|
437
|
+
// draw: drop them instead of printing "?" in the middle of Latin text.
|
|
438
|
+
if ((cp >= 0x200B && cp <= 0x200F) || cp === 0x2060 || cp === 0xFEFF) continue;
|
|
439
|
+
out += "?";
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
// The characters (each once, in document order) that toWinAnsi would write as "?": what a vector export of the
|
|
444
|
+
// serialized document, its title and its running header / footer would lose.
|
|
445
|
+
function unsupportedText(options, ignoreFont) {
|
|
446
|
+
var text = "";
|
|
447
|
+
try {
|
|
448
|
+
var box = document.createElement("div");
|
|
449
|
+
box.innerHTML = __RTE_TT(editor.getHTMLCode() || "");
|
|
450
|
+
text = box.textContent || "";
|
|
451
|
+
} catch (e) { text = ""; }
|
|
452
|
+
text += " " + (options.title || config.pdfExportTitle || "") + " " + (options.headerText || config.pdfExportHeader || "") +
|
|
453
|
+
" " + (options.footerText || config.pdfExportFooter || "");
|
|
454
|
+
var seen = {}, out = "";
|
|
455
|
+
var uniFont = ignoreFont ? null : uniFontFrom(options);
|
|
456
|
+
for (var i = 0; i < text.length; i++) {
|
|
457
|
+
var ch = text.charAt(i), code = text.charCodeAt(i);
|
|
458
|
+
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < text.length) { ch = text.substr(i, 2); i++; }
|
|
459
|
+
if (seen[ch] || ch === "?") continue;
|
|
460
|
+
if (toWinAnsi(ch).indexOf("?") >= 0 && !(uniFont && uniFont.gidFor(ch.codePointAt(0)))) { seen[ch] = 1; out += ch; }
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
// 2026-10-26 A PDF text string (title, author, bookmark, alt text, link or comment description) is not drawn with
|
|
465
|
+
// a font, so it can carry any script as UTF-16BE: <FEFF...>. These went through WinAnsi like the page text and a
|
|
466
|
+
// Chinese title, Russian bookmark or Japanese alt text - what a screen reader announces - became "???". Strings
|
|
467
|
+
// WinAnsi can carry keep the plain (...) form. tools/pdf/pdf-unicode-strings.mjs
|
|
468
|
+
function pdfText(str) {
|
|
469
|
+
var t = String(str == null ? "" : str);
|
|
470
|
+
var win = toWinAnsi(t);
|
|
471
|
+
if (win.indexOf("?") < 0 || win.split("?").length === t.split("?").length) return "(" + escapeString(win) + ")";
|
|
472
|
+
var hex = "<FEFF";
|
|
473
|
+
for (var i = 0; i < t.length; i++) hex += ("000" + t.charCodeAt(i).toString(16).toUpperCase()).slice(-4);
|
|
474
|
+
return hex + ">";
|
|
475
|
+
}
|
|
476
|
+
// A URI is ASCII: percent-encode anything else (it became "?").
|
|
477
|
+
function pdfUri(href) {
|
|
478
|
+
return String(href || "").replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\x00-\x7F]/g, function (c) {
|
|
479
|
+
try { return encodeURIComponent(c); } catch (e) { return ""; }
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function escapeString(str) {
|
|
483
|
+
return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ------------------------------------------------------------- PDF writer
|
|
487
|
+
function PdfWriter() {
|
|
488
|
+
this.objects = []; // 1-based; objects[i] is the body of object i+1
|
|
489
|
+
}
|
|
490
|
+
PdfWriter.prototype.alloc = function () { this.objects.push(null); return this.objects.length; };
|
|
491
|
+
PdfWriter.prototype.set = function (id, body) { this.objects[id - 1] = body; };
|
|
492
|
+
PdfWriter.prototype.add = function (body) { var id = this.alloc(); this.set(id, body); return id; };
|
|
493
|
+
|
|
494
|
+
// Assembled as BYTES, not as a string: a content stream can hold binary
|
|
495
|
+
// (an embedded JPEG), and /Length plus the xref offsets are byte counts. A
|
|
496
|
+
// string-based assembler is correct right up until the first image, then
|
|
497
|
+
// silently produces a file no reader will open.
|
|
498
|
+
PdfWriter.prototype.build = function (rootId, infoId) {
|
|
499
|
+
var chunks = [], length = 0;
|
|
500
|
+
function push(part) {
|
|
501
|
+
var bytes = (typeof part === "string") ? latin1Bytes(part) : part;
|
|
502
|
+
chunks.push(bytes); length += bytes.length;
|
|
503
|
+
return length;
|
|
504
|
+
}
|
|
505
|
+
push("%PDF-1.4\n");
|
|
506
|
+
// A binary comment marks the file as binary for transfer agents.
|
|
507
|
+
push(new Uint8Array([0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A]));
|
|
508
|
+
|
|
509
|
+
var offsets = [];
|
|
510
|
+
for (var i = 0; i < this.objects.length; i++) {
|
|
511
|
+
offsets[i] = length;
|
|
512
|
+
push((i + 1) + " 0 obj\n");
|
|
513
|
+
var body = this.objects[i];
|
|
514
|
+
if (body && body.stream) {
|
|
515
|
+
var data = (typeof body.stream === "string") ? latin1Bytes(body.stream) : body.stream;
|
|
516
|
+
push(body.dict.replace("/Length 0", "/Length " + data.length) + "\nstream\n");
|
|
517
|
+
push(data);
|
|
518
|
+
push("\nendstream\n");
|
|
519
|
+
} else {
|
|
520
|
+
push(body + "\n");
|
|
521
|
+
}
|
|
522
|
+
push("endobj\n");
|
|
523
|
+
}
|
|
524
|
+
var xrefAt = length;
|
|
525
|
+
var xref = "xref\n0 " + (this.objects.length + 1) + "\n0000000000 65535 f \n";
|
|
526
|
+
for (var j = 0; j < offsets.length; j++) {
|
|
527
|
+
xref += pad10(offsets[j]) + " 00000 n \n";
|
|
528
|
+
}
|
|
529
|
+
push(xref);
|
|
530
|
+
push("trailer\n<< /Size " + (this.objects.length + 1) + " /Root " + rootId + " 0 R /Info " + infoId + " 0 R >>\n");
|
|
531
|
+
push("startxref\n" + xrefAt + "\n%%EOF\n");
|
|
532
|
+
|
|
533
|
+
var out = new Uint8Array(length), at = 0;
|
|
534
|
+
for (var k = 0; k < chunks.length; k++) { out.set(chunks[k], at); at += chunks[k].length; }
|
|
535
|
+
return out;
|
|
536
|
+
};
|
|
537
|
+
function pad10(n) { var s = String(n); while (s.length < 10) s = "0" + s; return s; }
|
|
538
|
+
function latin1Bytes(str) {
|
|
539
|
+
var out = new Uint8Array(str.length);
|
|
540
|
+
for (var i = 0; i < str.length; i++) out[i] = str.charCodeAt(i) & 0xFF;
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---------------------------------------------------------------- layout
|
|
545
|
+
//
|
|
546
|
+
// A flat list of positioned draw operations is produced first, then paged.
|
|
547
|
+
// Keeping measurement and pagination apart is what makes "keep this heading
|
|
548
|
+
// with the paragraph under it" and "repeat a table header row" possible at
|
|
549
|
+
// all; doing both in one pass forces every decision to be final immediately.
|
|
550
|
+
|
|
551
|
+
function styleOf(node, inherited) {
|
|
552
|
+
var s = {
|
|
553
|
+
bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
|
|
554
|
+
strike: inherited.strike, size: inherited.size, color: inherited.color,
|
|
555
|
+
mono: inherited.mono, link: inherited.link, lang: inherited.lang, comment: inherited.comment || null
|
|
556
|
+
};
|
|
557
|
+
// 2026-10-17 a run's language: the nearest lang attribute. tools/pdf/inline-lang.mjs
|
|
558
|
+
var nodeLang = node.getAttribute ? node.getAttribute("lang") : "";
|
|
559
|
+
if (nodeLang) s.lang = nodeLang;
|
|
560
|
+
var tag = node.tagName ? node.tagName.toLowerCase() : "";
|
|
561
|
+
if (tag === "b" || tag === "strong") s.bold = true;
|
|
562
|
+
if (tag === "i" || tag === "em") s.italic = true;
|
|
563
|
+
if (tag === "u" || tag === "ins") s.underline = true;
|
|
564
|
+
if (tag === "s" || tag === "strike" || tag === "del") s.strike = true;
|
|
565
|
+
if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
|
|
566
|
+
if (tag === "a" && node.getAttribute("href")) { s.link = node.getAttribute("href"); s.color = [0, 0.33, 0.8]; s.underline = true; }
|
|
567
|
+
if (node.getAttribute && node.getAttribute("data-comment")) s.comment = { text: node.getAttribute("data-comment"), author: node.getAttribute("data-comment-author") || "" };
|
|
568
|
+
if (tag === "sup" || tag === "sub") s.size = s.size * 0.75;
|
|
569
|
+
|
|
570
|
+
var css = node.style;
|
|
571
|
+
if (css) {
|
|
572
|
+
if (/bold|^[6-9]00$/.test(css.fontWeight || "")) s.bold = true;
|
|
573
|
+
if (css.fontStyle === "italic" || css.fontStyle === "oblique") s.italic = true;
|
|
574
|
+
if ((css.textDecorationLine || css.textDecoration || "").indexOf("underline") >= 0) s.underline = true;
|
|
575
|
+
if ((css.textDecorationLine || css.textDecoration || "").indexOf("line-through") >= 0) s.strike = true;
|
|
576
|
+
if (css.color) { var c = parseColor(css.color); if (c) s.color = c; }
|
|
577
|
+
if (css.fontSize) { var fs = toPoints(css.fontSize, null); if (fs) s.size = fs; }
|
|
578
|
+
}
|
|
579
|
+
return s;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function parseColor(value) {
|
|
583
|
+
var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.replace(/\s/g, ""));
|
|
584
|
+
if (m) {
|
|
585
|
+
var hex = m[1];
|
|
586
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
587
|
+
return [parseInt(hex.substr(0, 2), 16) / 255, parseInt(hex.substr(2, 2), 16) / 255, parseInt(hex.substr(4, 2), 16) / 255];
|
|
588
|
+
}
|
|
589
|
+
var rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value);
|
|
590
|
+
if (rgb) return [rgb[1] / 255, rgb[2] / 255, rgb[3] / 255];
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
var HEADING_SCALE = { h1: 2, h2: 1.6, h3: 1.35, h4: 1.15, h5: 1, h6: 0.9 };
|
|
595
|
+
|
|
596
|
+
function Layout(geom, baseSize, familyKey) {
|
|
597
|
+
this.geom = geom;
|
|
598
|
+
this.baseSize = baseSize;
|
|
599
|
+
this.family = familyKey;
|
|
600
|
+
this.contentWidth = geom.width - geom.margin.left - geom.margin.right;
|
|
601
|
+
this.blocks = []; // { type, ... } in document order
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Collect the inline runs of a block, splitting into words for wrapping.
|
|
605
|
+
Layout.prototype.runsOf = function (node, inherited) {
|
|
606
|
+
var runs = [];
|
|
607
|
+
var self = this;
|
|
608
|
+
(function walk(n, style) {
|
|
609
|
+
for (var i = 0; i < n.childNodes.length; i++) {
|
|
610
|
+
var c = n.childNodes[i];
|
|
611
|
+
if (c.nodeType === 3) {
|
|
612
|
+
var text = (c.nodeValue || "").replace(/\s+/g, " ");
|
|
613
|
+
if (text) runs.push({ text: text, style: style });
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (c.nodeType !== 1) continue;
|
|
617
|
+
var tag = c.tagName.toLowerCase();
|
|
618
|
+
if (tag === "br") { runs.push({ br: true, style: style }); continue; }
|
|
619
|
+
if (tag === "img") { runs.push({ img: c, style: style }); continue; }
|
|
620
|
+
if (tag === "script" || tag === "style") continue;
|
|
621
|
+
walk(c, styleOf(c, style));
|
|
622
|
+
}
|
|
623
|
+
})(node, inherited);
|
|
624
|
+
return runs;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Break runs into laid-out lines that fit `width`.
|
|
628
|
+
Layout.prototype.wrap = function (runs, width, lineHeightFactor) {
|
|
629
|
+
var lines = [], line = [], used = 0, self = this;
|
|
630
|
+
var maxSize = this.baseSize;
|
|
631
|
+
|
|
632
|
+
function flush() {
|
|
633
|
+
// Trailing spaces must not count toward a line's width, or a
|
|
634
|
+
// right-aligned or centred line drifts by the width of the space.
|
|
635
|
+
while (line.length && /^\s+$/.test(line[line.length - 1].text || "")) line.pop();
|
|
636
|
+
lines.push({ parts: line, width: used, size: maxSize });
|
|
637
|
+
line = []; used = 0; maxSize = self.baseSize;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (var r = 0; r < runs.length; r++) {
|
|
641
|
+
var run = runs[r];
|
|
642
|
+
if (run.br) { flush(); continue; }
|
|
643
|
+
if (run.img) {
|
|
644
|
+
var dims = imageDims(run.img, width);
|
|
645
|
+
if (line.length) flush();
|
|
646
|
+
lines.push({ parts: [{ img: run.img, w: dims.w, h: dims.h, style: run.style }], width: dims.w, size: dims.h, image: true });
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
var styleIndex = (run.style.bold ? 1 : 0) | (run.style.italic ? 2 : 0);
|
|
650
|
+
var family = run.style.mono ? "courier" : this.family;
|
|
651
|
+
// Split keeping the separators, so a space that ends a line can be
|
|
652
|
+
// dropped while a space between two words on the same line is kept.
|
|
653
|
+
var tokens = run.text.split(/(\s+)/);
|
|
654
|
+
for (var t = 0; t < tokens.length; t++) {
|
|
655
|
+
var whole = tokens[t];
|
|
656
|
+
if (!whole) continue;
|
|
657
|
+
var pieces = activeUni && !/^\s+$/.test(whole) ? uniPieces(whole) : [{ text: whole, uni: false }];
|
|
658
|
+
for (var pc = 0; pc < pieces.length; pc++) {
|
|
659
|
+
var tok = pieces[pc].text, isUni = pieces[pc].uni;
|
|
660
|
+
var w = isUni ? uniWidth(tok, run.style.size) : textWidth(toWinAnsi(tok), family, styleIndex, run.style.size);
|
|
661
|
+
// a piece that continues a word (no space before it) may still wrap: CJK breaks anywhere
|
|
662
|
+
if (used + w > width && line.length && !/^\s+$/.test(tok) && (pc === 0 || isUni)) {
|
|
663
|
+
flush();
|
|
664
|
+
// A wrapped line never starts with the space that caused it.
|
|
665
|
+
if (/^\s+$/.test(tok)) continue;
|
|
666
|
+
}
|
|
667
|
+
line.push({ text: tok, style: run.style, family: family, styleIndex: styleIndex, w: w, uni: isUni });
|
|
668
|
+
used += w;
|
|
669
|
+
if (run.style.size > maxSize) maxSize = run.style.size;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (line.length) flush();
|
|
674
|
+
if (!lines.length) lines.push({ parts: [], width: 0, size: this.baseSize });
|
|
675
|
+
for (var i = 0; i < lines.length; i++) lines[i].height = lines[i].image ? lines[i].size : lines[i].size * lineHeightFactor;
|
|
676
|
+
return lines;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
function imageDims(img, maxWidth) {
|
|
680
|
+
var w = img.naturalWidth || img.width || 0;
|
|
681
|
+
var h = img.naturalHeight || img.height || 0;
|
|
682
|
+
if (!w || !h) return { w: 0, h: 0 };
|
|
683
|
+
var pw = w * 0.75, ph = h * 0.75; // CSS px -> points
|
|
684
|
+
if (pw > maxWidth) { ph = ph * maxWidth / pw; pw = maxWidth; }
|
|
685
|
+
return { w: pw, h: ph };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ------------------------------------------------------------ block walk
|
|
689
|
+
Layout.prototype.run = function (root) {
|
|
690
|
+
var self = this;
|
|
691
|
+
var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
|
|
692
|
+
|
|
693
|
+
// A block indented in the editor carries margin-left (or padding-left, per
|
|
694
|
+
// indentUseMargin). Before 2026-09-04 the PDF honoured an indent only when
|
|
695
|
+
// it was spelled <blockquote>, because that was the only way the editor
|
|
696
|
+
// could express one. Points, since that is the PDF's unit: 1px = 0.75pt.
|
|
697
|
+
function cssIndentPt(node) {
|
|
698
|
+
if (!node || !node.style) return 0;
|
|
699
|
+
var raw = node.style.marginLeft || node.style.paddingLeft || "";
|
|
700
|
+
var m = /^\s*(-?[\d.]+)\s*([a-z%]*)\s*$/i.exec(raw);
|
|
701
|
+
if (!m) return 0;
|
|
702
|
+
var n = parseFloat(m[1]);
|
|
703
|
+
if (!n || n <= 0) return 0;
|
|
704
|
+
var unit = (m[2] || "px").toLowerCase();
|
|
705
|
+
var px = unit === "px" ? n
|
|
706
|
+
: unit === "pt" ? n * (4 / 3)
|
|
707
|
+
: unit === "in" ? n * 96
|
|
708
|
+
: unit === "cm" ? n * 37.795
|
|
709
|
+
: unit === "mm" ? n * 3.7795
|
|
710
|
+
: (unit === "em" || unit === "rem") ? n * 16
|
|
711
|
+
: 0;
|
|
712
|
+
return px * 0.75;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// The language a block is written in: its own lang, the nearest ancestor's inside the content, or - when its
|
|
716
|
+
// whole text is one <span lang> - that span's. "" when none is set.
|
|
717
|
+
function blockLang(node) {
|
|
718
|
+
for (var n = node; n && n.nodeType === 1 && n.tagName.toLowerCase() !== "body"; n = n.parentNode) {
|
|
719
|
+
if (n.getAttribute("lang")) return n.getAttribute("lang");
|
|
720
|
+
}
|
|
721
|
+
var el = null, k = node.childNodes;
|
|
722
|
+
for (var i = 0; i < k.length; i++) {
|
|
723
|
+
if (k[i].nodeType === 3 && k[i].nodeValue.replace(/[\s\u200b\u00a0]/g, "")) return "";
|
|
724
|
+
if (k[i].nodeType === 1) { if (el) return ""; el = k[i]; }
|
|
725
|
+
}
|
|
726
|
+
return el && el.getAttribute("lang") ? el.getAttribute("lang") : "";
|
|
727
|
+
}
|
|
728
|
+
function block(node, style, indent, listMarker, listId, listOrdered, roleOverride) {
|
|
729
|
+
var tag = node.tagName.toLowerCase();
|
|
730
|
+
var s = styleOf(node, style);
|
|
731
|
+
var align = "left";
|
|
732
|
+
var ta = (node.style && node.style.textAlign) || "";
|
|
733
|
+
if (ta === "center" || ta === "right" || ta === "justify") align = ta;
|
|
734
|
+
|
|
735
|
+
if (HEADING_SCALE[tag]) { s.size = self.baseSize * HEADING_SCALE[tag]; s.bold = true; }
|
|
736
|
+
if (tag === "pre") s.mono = true;
|
|
737
|
+
|
|
738
|
+
var width = self.contentWidth - indent;
|
|
739
|
+
var runs = self.runsOf(node, s);
|
|
740
|
+
var lines = self.wrap(runs, width, tag === "pre" ? 1.25 : 1.4);
|
|
741
|
+
self.blocks.push({
|
|
742
|
+
type: "text", lines: lines, indent: indent, align: align,
|
|
743
|
+
// 2026-10-15 the block's own language, for /Lang on its structure element. tools/pdf/paragraph-lang.mjs
|
|
744
|
+
lang: blockLang(node),
|
|
745
|
+
spaceBefore: HEADING_SCALE[tag] ? s.size * 0.6 : self.baseSize * 0.35,
|
|
746
|
+
spaceAfter: HEADING_SCALE[tag] ? s.size * 0.3 : self.baseSize * 0.35,
|
|
747
|
+
marker: listMarker || null, markerStyle: s,
|
|
748
|
+
// A heading alone at the foot of a page is the classic ugly
|
|
749
|
+
// break; it is pinned to what follows it.
|
|
750
|
+
keepWithNext: !!HEADING_SCALE[tag],
|
|
751
|
+
rule: tag === "blockquote" ? "quote" : null,
|
|
752
|
+
// Semantics for the tag tree. A PDF whose text is selectable but
|
|
753
|
+
// untagged still fails an accessibility audit: assistive
|
|
754
|
+
// technology gets a flat stream with no headings, no list
|
|
755
|
+
// structure and no reliable reading order.
|
|
756
|
+
// roleOverride carries the quotation down to the paragraphs INSIDE a
|
|
757
|
+
// <blockquote>. The role was only ever emitted for a blockquote
|
|
758
|
+
// holding inline text directly - and the editor writes
|
|
759
|
+
// <blockquote><p>...</p></blockquote>, so in practice every
|
|
760
|
+
// blockquote we produce was tagged /P and the quotation was absent
|
|
761
|
+
// from the accessibility tree we publish claims about.
|
|
762
|
+
role: roleOverride || (HEADING_SCALE[tag] ? "H" + tag.charAt(1) : (tag === "blockquote" ? "BlockQuote" : "P")),
|
|
763
|
+
listId: listId || null, listOrdered: !!listOrdered
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function walk(node, style, indent, roleOverride) {
|
|
768
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
769
|
+
var c = node.childNodes[i];
|
|
770
|
+
if (c.nodeType === 3) {
|
|
771
|
+
if (!(c.nodeValue || "").trim()) continue;
|
|
772
|
+
// Loose text directly under the root still deserves a block.
|
|
773
|
+
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 });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (c.nodeType !== 1) continue;
|
|
777
|
+
var tag = c.tagName.toLowerCase();
|
|
778
|
+
if (tag === "script" || tag === "style" || tag === "noscript") continue;
|
|
779
|
+
|
|
780
|
+
if (c.getAttribute && c.getAttribute("data-rte-page-break") !== null) { self.blocks.push({ type: "pagebreak" }); continue; }
|
|
781
|
+
if (c.style && /always|page/.test(c.style.pageBreakBefore || c.style.breakBefore || "")) self.blocks.push({ type: "pagebreak" });
|
|
782
|
+
|
|
783
|
+
if (tag === "hr") { self.blocks.push({ type: "rule", indent: indent }); continue; }
|
|
784
|
+
if (tag === "ul" || tag === "ol") {
|
|
785
|
+
var counter = parseInt(c.getAttribute("start"), 10);
|
|
786
|
+
if (isNaN(counter)) counter = 1;
|
|
787
|
+
// 2026-10-04 a lettered / Roman list is numbered the way the editor shows it (it was
|
|
788
|
+
// always 1. 2. 3.). tools/pdf/list-markers.mjs
|
|
789
|
+
var lst = String((c.style && c.style.listStyleType) || "").toLowerCase(), typ = c.getAttribute("type") || "";
|
|
790
|
+
var kind = /^(lower-alpha|lower-latin)$/.test(lst) || typ === "a" ? "a"
|
|
791
|
+
: /^(upper-alpha|upper-latin)$/.test(lst) || typ === "A" ? "A"
|
|
792
|
+
: lst === "lower-roman" || typ === "i" ? "i"
|
|
793
|
+
: lst === "upper-roman" || typ === "I" ? "I" : "1";
|
|
794
|
+
// Every item of one list shares an id so the tag tree can
|
|
795
|
+
// rebuild a single <L> around them; without it each item
|
|
796
|
+
// becomes an isolated paragraph and a screen reader never
|
|
797
|
+
// announces "list, 4 items".
|
|
798
|
+
var listId = "L" + (self.listSeq = (self.listSeq || 0) + 1);
|
|
799
|
+
for (var li = 0; li < c.childNodes.length; li++) {
|
|
800
|
+
var item = c.childNodes[li];
|
|
801
|
+
if (item.nodeType !== 1 || item.tagName.toLowerCase() !== "li") continue;
|
|
802
|
+
var marker = tag === "ol" ? listNumber(counter++, kind) + "." : "\u2022";
|
|
803
|
+
// 2026-10-15 a task item shows whether it is done: every item got the plain bullet, so a printed
|
|
804
|
+
// checklist lost every tick. The standard fonts have no ballot box, so "[x]" / "[ ]".
|
|
805
|
+
// tools/pdf/task-list-marks.mjs
|
|
806
|
+
if (c.classList && c.classList.contains("rte-task-list")) marker = item.getAttribute("data-checked") === "true" ? "[x]" : "[ ]";
|
|
807
|
+
// The item's own text, then any nested list one level in.
|
|
808
|
+
block(item, styleOf(item, style), indent + self.baseSize * 1.6, marker, listId, tag === "ol");
|
|
809
|
+
walkNestedLists(item, styleOf(item, style), indent + self.baseSize * 1.6);
|
|
810
|
+
}
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (tag === "table") { self.table(c, styleOf(c, style), indent); continue; }
|
|
814
|
+
if (tag === "img") {
|
|
815
|
+
var dims = imageDims(c, self.contentWidth - indent);
|
|
816
|
+
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 });
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (isBlockTag(tag)) {
|
|
820
|
+
var own = (tag === "blockquote" ? self.baseSize * 1.5 : 0) + cssIndentPt(c);
|
|
821
|
+
var childRole = (tag === "blockquote") ? "BlockQuote" : roleOverride;
|
|
822
|
+
// 2026-10-15 a block holding just an image - the shape the editor gives every inserted picture,
|
|
823
|
+
// <p><img></p> - is a Figure with its alt text. Only an <img> sitting directly at block level was;
|
|
824
|
+
// inside a <p> it was an inline run, so the PDF had no Figure and no /Alt (WCAG 1.1.1, PDF/UA).
|
|
825
|
+
// tools/pdf/figure-alt.mjs
|
|
826
|
+
var soleImgs = c.getElementsByTagName("img");
|
|
827
|
+
if (soleImgs.length === 1 && !(c.textContent || "").replace(/[\s\u200b\u00a0]/g, "") && !hasBlockChildren(c)) {
|
|
828
|
+
var fdims = imageDims(soleImgs[0], self.contentWidth - indent - own);
|
|
829
|
+
if (fdims.w) {
|
|
830
|
+
var falign = String((c.style && c.style.textAlign) || "").toLowerCase();
|
|
831
|
+
self.blocks.push({ type: "text", role: "Figure", alt: soleImgs[0].getAttribute("alt") || "", lines: [{ parts: [{ img: soleImgs[0], w: fdims.w, h: fdims.h, style: style }], width: fdims.w, size: fdims.h, height: fdims.h, image: true }], indent: indent + own, align: falign === "center" || falign === "right" ? falign : "left", spaceBefore: 4, spaceAfter: 4 });
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + own, childRole); continue; }
|
|
836
|
+
block(c, style, indent + own, null, null, false, childRole);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
// Inline content sitting directly under a container.
|
|
840
|
+
block(c, style, indent, null);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function walkNestedLists(item, style, indent) {
|
|
845
|
+
for (var i = 0; i < item.childNodes.length; i++) {
|
|
846
|
+
var c = item.childNodes[i];
|
|
847
|
+
if (c.nodeType !== 1) continue;
|
|
848
|
+
var tag = c.tagName.toLowerCase();
|
|
849
|
+
if (tag === "ul" || tag === "ol") walk({ childNodes: [c] }, style, indent);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
walk(root, base, 0);
|
|
854
|
+
return this.blocks;
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
function isBlockTag(tag) {
|
|
858
|
+
return /^(p|div|h[1-6]|blockquote|pre|section|article|header|footer|main|aside|figure|figcaption|dl|dd|dt|li|address)$/.test(tag);
|
|
859
|
+
}
|
|
860
|
+
function hasBlockChildren(node) {
|
|
861
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
862
|
+
var c = node.childNodes[i];
|
|
863
|
+
if (c.nodeType === 1 && (isBlockTag(c.tagName.toLowerCase()) || /^(ul|ol|table|hr)$/.test(c.tagName.toLowerCase()))) return true;
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
Layout.prototype.table = function (node, style, indent) {
|
|
869
|
+
var self = this;
|
|
870
|
+
var rows = [];
|
|
871
|
+
var trs = node.getElementsByTagName("tr");
|
|
872
|
+
var maxCells = 0;
|
|
873
|
+
// 2026-10-15 rowspan: covered[col] = rows a cell above still covers there. Cells in the rows below skip those
|
|
874
|
+
// columns (they were placed from column 0, shifting a row left under the wrong header), and each covered
|
|
875
|
+
// position becomes an empty placeholder cell so the grid stays aligned. tools/pdf/table-rowspan.mjs
|
|
876
|
+
var covered = {};
|
|
877
|
+
for (var r = 0; r < trs.length; r++) {
|
|
878
|
+
var cells = [];
|
|
879
|
+
for (var c = 0; c < trs[r].childNodes.length; c++) {
|
|
880
|
+
var cell = trs[r].childNodes[c];
|
|
881
|
+
if (cell.nodeType !== 1) continue;
|
|
882
|
+
var tag = cell.tagName.toLowerCase();
|
|
883
|
+
if (tag !== "td" && tag !== "th") continue;
|
|
884
|
+
// 2026-10-15 colspan: a cell starts at its grid column and spans its columns. tools/pdf/table-colspan.mjs
|
|
885
|
+
var span = Math.max(1, Math.min(50, parseInt(cell.getAttribute("colspan"), 10) || 1));
|
|
886
|
+
var rspan = Math.max(1, Math.min(500, parseInt(cell.getAttribute("rowspan"), 10) || 1));
|
|
887
|
+
cells.push({ node: cell, header: tag === "th", span: span, rowspan: rspan });
|
|
888
|
+
}
|
|
889
|
+
var placed = [], gridCols = 0;
|
|
890
|
+
for (var gc = 0; gc < cells.length; gc++) {
|
|
891
|
+
while (covered[gridCols] > 0) { placed.push({ placeholder: true, start: gridCols, span: 1, header: false }); gridCols++; }
|
|
892
|
+
cells[gc].start = gridCols;
|
|
893
|
+
placed.push(cells[gc]);
|
|
894
|
+
if (cells[gc].rowspan > 1) for (var rc = gridCols; rc < gridCols + cells[gc].span; rc++) covered[rc] = cells[gc].rowspan;
|
|
895
|
+
gridCols += cells[gc].span;
|
|
896
|
+
}
|
|
897
|
+
// covered columns after the row's last cell
|
|
898
|
+
for (var tail in covered) if (+tail >= gridCols && covered[tail] > 0) { placed.push({ placeholder: true, start: +tail, span: 1, header: false }); if (+tail + 1 > gridCols) gridCols = +tail + 1; }
|
|
899
|
+
// this row is done: each covering cell has one row fewer left (a cell covering from THIS row still needs
|
|
900
|
+
// its own count, so it was set to rowspan and drops to rowspan - 1 here)
|
|
901
|
+
for (var dc in covered) { covered[dc]--; if (covered[dc] <= 0) delete covered[dc]; }
|
|
902
|
+
placed.sort(function (p1, p2) { return p1.start - p2.start; });
|
|
903
|
+
cells = cells.length ? placed : cells;
|
|
904
|
+
if (cells.length) { rows.push(cells); if (gridCols > maxCells) maxCells = gridCols; }
|
|
905
|
+
}
|
|
906
|
+
if (!rows.length) return;
|
|
907
|
+
|
|
908
|
+
var width = this.contentWidth - indent;
|
|
909
|
+
var colWidth = width / maxCells;
|
|
910
|
+
// 2026-10-15 column widths follow the <colgroup> (% or px), scaled to the table's width on the page: every
|
|
911
|
+
// table was split into equal columns. A table without one keeps equal columns. tools/pdf/table-col-widths.mjs
|
|
912
|
+
var colWidths = [], cg = [], cw = 0;
|
|
913
|
+
for (var ck = 0; ck < node.children.length; ck++) if (node.children[ck].tagName && node.children[ck].tagName.toLowerCase() === "colgroup") {
|
|
914
|
+
for (var cj = 0; cj < node.children[ck].children.length; cj++) if (node.children[ck].children[cj].tagName.toLowerCase() === "col") cg.push(node.children[ck].children[cj]);
|
|
915
|
+
}
|
|
916
|
+
if (cg.length === maxCells) {
|
|
917
|
+
for (var cq = 0; cq < cg.length; cq++) {
|
|
918
|
+
var cv = (cg[cq].style && cg[cq].style.width) || cg[cq].getAttribute("width") || "", cm = /^\s*([\d.]+)(%|px)?\s*$/.exec(cv);
|
|
919
|
+
if (!cm || !(parseFloat(cm[1]) > 0)) { colWidths = []; break; }
|
|
920
|
+
colWidths.push(parseFloat(cm[1]));
|
|
921
|
+
}
|
|
922
|
+
for (var cs2 = 0; cs2 < colWidths.length; cs2++) cw += colWidths[cs2];
|
|
923
|
+
if (cw > 0) colWidths = colWidths.map(function (v) { return v / cw * width; });
|
|
924
|
+
}
|
|
925
|
+
if (colWidths.length !== maxCells) { colWidths = []; for (var ce = 0; ce < maxCells; ce++) colWidths.push(colWidth); }
|
|
926
|
+
function spanWidth(start, span) { var s = 0; for (var sw = start; sw < start + span && sw < colWidths.length; sw++) s += colWidths[sw]; return s; }
|
|
927
|
+
var pad = 4;
|
|
928
|
+
var laid = [];
|
|
929
|
+
for (var i = 0; i < rows.length; i++) {
|
|
930
|
+
var out = [], tallest = 0;
|
|
931
|
+
for (var j = 0; j < rows[i].length; j++) {
|
|
932
|
+
if (rows[i][j].placeholder) { out.push({ lines: [], header: false, start: rows[i][j].start, span: 1, placeholder: true }); continue; }
|
|
933
|
+
var cs = styleOf(rows[i][j].node, style);
|
|
934
|
+
if (rows[i][j].header) cs.bold = true;
|
|
935
|
+
var lines = self.wrap(self.runsOf(rows[i][j].node, cs), spanWidth(rows[i][j].start, rows[i][j].span) - pad * 2, 1.35);
|
|
936
|
+
var h = 0;
|
|
937
|
+
for (var k = 0; k < lines.length; k++) h += lines[k].height;
|
|
938
|
+
if (h > tallest) tallest = h;
|
|
939
|
+
out.push({ lines: lines, header: rows[i][j].header, start: rows[i][j].start, span: rows[i][j].span, rowspan: rows[i][j].rowspan || 1 });
|
|
940
|
+
}
|
|
941
|
+
laid.push({ cells: out, height: tallest + pad * 2, header: rows[i][0] && !rows[i][0].placeholder && rows[i][0].header });
|
|
942
|
+
}
|
|
943
|
+
this.blocks.push({
|
|
944
|
+
type: "table", rows: laid, colWidth: colWidth, colWidths: colWidths, cols: maxCells,
|
|
945
|
+
indent: indent, pad: pad,
|
|
946
|
+
spaceBefore: this.baseSize * 0.5, spaceAfter: this.baseSize * 0.5
|
|
947
|
+
});
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// ------------------------------------------------------------- rendering
|
|
951
|
+
// 2026-10-15 the document language, read by the tagger to mark blocks in other languages
|
|
952
|
+
var pdfDocLang = "en";
|
|
953
|
+
// Marks each live comment's text in the copy as a data-comment span (see buildPdf).
|
|
954
|
+
function markCommentsForPdf(host) {
|
|
955
|
+
var live = editor.getEditable().querySelectorAll("[data-comment-id]");
|
|
956
|
+
if (!live.length || !editor.reviewLedger) return;
|
|
957
|
+
var ledger = {};
|
|
958
|
+
editor.reviewLedger.list().forEach(function (e) { if (e.changeType === "comment") ledger[e.id] = e; });
|
|
959
|
+
var done = {};
|
|
960
|
+
for (var i = 0; i < live.length; i++) {
|
|
961
|
+
var id = live[i].getAttribute("data-comment-id"), entry = ledger[id], phrase = live[i].textContent || "";
|
|
962
|
+
if (!entry || !phrase || done[id]) continue;
|
|
963
|
+
done[id] = true;
|
|
964
|
+
var w = document.createTreeWalker(host, NodeFilter.SHOW_TEXT, null, false), tn, at = -1;
|
|
965
|
+
while ((tn = w.nextNode())) { at = tn.data.indexOf(phrase); if (at !== -1) break; }
|
|
966
|
+
if (at === -1) continue;
|
|
967
|
+
var rng = document.createRange();
|
|
968
|
+
rng.setStart(tn, at); rng.setEnd(tn, at + phrase.length);
|
|
969
|
+
var name = (entry.author && entry.author.name) || "RichTextEditor user";
|
|
970
|
+
var mark = document.createElement("span");
|
|
971
|
+
mark.setAttribute("data-comment", entry.text || "");
|
|
972
|
+
mark.setAttribute("data-comment-author", name);
|
|
973
|
+
try { rng.surroundContents(mark); } catch (er) { /* no mark */ }
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function uniFontFrom(options) {
|
|
977
|
+
var src = (options && options.unicodeFont) || config.pdfExportUnicodeFont;
|
|
978
|
+
if (!src) return null;
|
|
979
|
+
try { return parseTrueType(src); }
|
|
980
|
+
catch (e) { if (window.console && console.warn) console.warn(e.message); return null; }
|
|
981
|
+
}
|
|
982
|
+
function buildPdf(options) {
|
|
983
|
+
activeUni = null;
|
|
984
|
+
var uniFont = uniFontFrom(options);
|
|
985
|
+
if (uniFont && unsupportedText(options, true)) activeUni = { font: uniFont, used: {} };
|
|
986
|
+
try { return buildPdfWith(options); } finally { activeUni = null; }
|
|
987
|
+
}
|
|
988
|
+
function buildPdfWith(options) {
|
|
989
|
+
pdfDocLang = String((options && options.lang) || document.documentElement.lang || "en").toLowerCase();
|
|
990
|
+
var familyKey = String(options.font || config.pdfExportFont || "helvetica").toLowerCase();
|
|
991
|
+
if (!FONT_FAMILIES[familyKey]) familyKey = "helvetica";
|
|
992
|
+
var baseSize = Number(options.fontSize || config.pdfExportBaseFontSize || 11);
|
|
993
|
+
if (!isFinite(baseSize) || baseSize <= 0) baseSize = 11;
|
|
994
|
+
var geom = pageGeometry(options);
|
|
995
|
+
|
|
996
|
+
// getHTMLCode() is the SERIALIZED document: presentational plugins strip
|
|
997
|
+
// their own chrome from it. Reading the live DOM instead would bake page
|
|
998
|
+
// overlays, watermarks and formatting marks into the PDF.
|
|
999
|
+
var host = document.createElement("div");
|
|
1000
|
+
host.innerHTML = __RTE_TT(editor.getHTMLCode() || "");
|
|
1001
|
+
var liveLists = editor.getEditable().querySelectorAll("ol,ul"), exportLists = host.querySelectorAll("ol,ul");
|
|
1002
|
+
if (liveLists.length === exportLists.length) for (var li = 0; li < liveLists.length; li++) {
|
|
1003
|
+
var listStyle = liveLists[li].ownerDocument.defaultView.getComputedStyle(liveLists[li]).listStyleType;
|
|
1004
|
+
if (listStyle) exportLists[li].style.listStyleType = listStyle;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// 2026-10-21 comments reach the PDF as /Text notes: their anchors and text are marked in the copy first,
|
|
1008
|
+
// as in the Word export. tools/pdf/comments.mjs
|
|
1009
|
+
try { markCommentsForPdf(host); } catch (cmErr) { /* the PDF without notes */ }
|
|
1010
|
+
var layout = new Layout(geom, baseSize, familyKey);
|
|
1011
|
+
var blocks = layout.run(host);
|
|
1012
|
+
|
|
1013
|
+
var pages = paginate(blocks, geom, baseSize);
|
|
1014
|
+
return emit(pages, geom, familyKey, baseSize, options);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function paginate(blocks, geom, baseSize) {
|
|
1018
|
+
var usableTop = geom.height - geom.margin.top;
|
|
1019
|
+
var usableBottom = geom.margin.bottom;
|
|
1020
|
+
var pages = [], current = [], y = usableTop;
|
|
1021
|
+
|
|
1022
|
+
function newPage() { pages.push(current); current = []; y = usableTop; }
|
|
1023
|
+
|
|
1024
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
1025
|
+
var b = blocks[i];
|
|
1026
|
+
if (b.type === "pagebreak") { if (current.length) newPage(); continue; }
|
|
1027
|
+
|
|
1028
|
+
if (b.type === "rule") {
|
|
1029
|
+
if (y - baseSize < usableBottom) newPage();
|
|
1030
|
+
y -= baseSize * 0.6;
|
|
1031
|
+
current.push({ op: "rule", y: y, indent: b.indent });
|
|
1032
|
+
y -= baseSize * 0.6;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (b.type === "table") {
|
|
1037
|
+
y -= b.spaceBefore;
|
|
1038
|
+
for (var r = 0; r < b.rows.length; r++) {
|
|
1039
|
+
var row = b.rows[r];
|
|
1040
|
+
if (y - row.height < usableBottom) {
|
|
1041
|
+
newPage();
|
|
1042
|
+
// Repeat the header row on the continuation page, or the
|
|
1043
|
+
// rest of the table arrives as unlabelled numbers.
|
|
1044
|
+
if (b.rows[0] && b.rows[0].header && r > 0) {
|
|
1045
|
+
current.push({ op: "row", row: b.rows[0], y: y, block: b });
|
|
1046
|
+
y -= b.rows[0].height;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
current.push({ op: "row", row: row, y: y, block: b });
|
|
1050
|
+
y -= row.height;
|
|
1051
|
+
}
|
|
1052
|
+
y -= b.spaceAfter;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
y -= b.spaceBefore;
|
|
1057
|
+
for (var l = 0; l < b.lines.length; l++) {
|
|
1058
|
+
var line = b.lines[l];
|
|
1059
|
+
if (y - line.height < usableBottom) newPage();
|
|
1060
|
+
current.push({ op: "line", line: line, y: y, block: b, first: l === 0 });
|
|
1061
|
+
y -= line.height;
|
|
1062
|
+
}
|
|
1063
|
+
y -= b.spaceAfter;
|
|
1064
|
+
|
|
1065
|
+
// keepWithNext: if a heading ended up as the last thing on the page,
|
|
1066
|
+
// move it to the next one rather than orphaning it.
|
|
1067
|
+
if (b.keepWithNext && current.length && blocks[i + 1] && blocks[i + 1].type !== "pagebreak") {
|
|
1068
|
+
var nextHeight = estimateFirstLine(blocks[i + 1]);
|
|
1069
|
+
if (y - nextHeight < usableBottom) {
|
|
1070
|
+
var moved = [];
|
|
1071
|
+
while (current.length && current[current.length - 1].block === b) moved.unshift(current.pop());
|
|
1072
|
+
if (current.length) {
|
|
1073
|
+
newPage();
|
|
1074
|
+
for (var m = 0; m < moved.length; m++) {
|
|
1075
|
+
moved[m].y = y;
|
|
1076
|
+
current.push(moved[m]);
|
|
1077
|
+
y -= moved[m].line.height;
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
// Already at the top of a page: nothing to gain by moving.
|
|
1081
|
+
for (var m2 = 0; m2 < moved.length; m2++) current.push(moved[m2]);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (current.length || !pages.length) pages.push(current);
|
|
1087
|
+
return pages;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function estimateFirstLine(block) {
|
|
1091
|
+
if (block.type === "table") return block.rows[0] ? block.rows[0].height : 0;
|
|
1092
|
+
if (block.lines && block.lines[0]) return block.lines[0].height;
|
|
1093
|
+
return 0;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Type0 font over a CIDFontType2 subset: Identity-H codes are glyph IDs, /W carries the widths layout used,
|
|
1097
|
+
// and ToUnicode maps each glyph back to its character for copy, search and screen readers.
|
|
1098
|
+
function addUnicodeFont(pdf, uni) {
|
|
1099
|
+
var font = uni.font, scale = 1000 / font.units;
|
|
1100
|
+
var gids = Object.keys(uni.used).map(Number).sort(function (a, b) { return a - b; });
|
|
1101
|
+
var sub = subsetTrueType(font, uni.used);
|
|
1102
|
+
var fileId = pdf.add({ dict: "<< /Length1 " + sub.length + " /Length 0 >>", stream: sub });
|
|
1103
|
+
var name = "/RTEUNI+EmbeddedUnicode";
|
|
1104
|
+
var descId = pdf.add("<< /Type /FontDescriptor /FontName " + name + " /Flags 4 /FontBBox [" +
|
|
1105
|
+
font.bbox.map(function (v) { return Math.round(v * scale); }).join(" ") + "] /ItalicAngle " + font.italicAngle +
|
|
1106
|
+
" /Ascent " + Math.round(font.ascent * scale) + " /Descent " + Math.round(font.descent * scale) +
|
|
1107
|
+
" /CapHeight " + Math.round(font.capHeight * scale) + " /StemV 80 /FontFile2 " + fileId + " 0 R >>");
|
|
1108
|
+
var w = gids.map(function (g) { return g + " [" + Math.round(font.advance(g) * scale) + "]"; }).join(" ");
|
|
1109
|
+
var cidId = pdf.add("<< /Type /Font /Subtype /CIDFontType2 /BaseFont " + name +
|
|
1110
|
+
" /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor " + descId +
|
|
1111
|
+
" 0 R /DW 1000 /W [" + w + "] /CIDToGIDMap /Identity >>");
|
|
1112
|
+
var cmap = "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n" +
|
|
1113
|
+
"/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n" +
|
|
1114
|
+
"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n";
|
|
1115
|
+
for (var i = 0; i < gids.length; i += 100) {
|
|
1116
|
+
var chunk = gids.slice(i, i + 100);
|
|
1117
|
+
cmap += chunk.length + " beginbfchar\n";
|
|
1118
|
+
for (var j = 0; j < chunk.length; j++) {
|
|
1119
|
+
var cp = uni.used[chunk[j]], u = cp > 0xFFFF ? hex4(0xD800 + ((cp - 0x10000) >> 10)) + hex4(0xDC00 + ((cp - 0x10000) & 0x3FF)) : hex4(cp);
|
|
1120
|
+
cmap += "<" + hex4(chunk[j]) + "> <" + u + ">\n";
|
|
1121
|
+
}
|
|
1122
|
+
cmap += "endbfchar\n";
|
|
1123
|
+
}
|
|
1124
|
+
cmap += "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend";
|
|
1125
|
+
var tuId = pdf.add({ dict: "<< /Length 0 >>", stream: cmap });
|
|
1126
|
+
return pdf.add("<< /Type /Font /Subtype /Type0 /BaseFont " + name + " /Encoding /Identity-H /DescendantFonts [" +
|
|
1127
|
+
cidId + " 0 R] /ToUnicode " + tuId + " 0 R >>");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function emit(pages, geom, familyKey, baseSize, options) {
|
|
1131
|
+
var pdf = new PdfWriter();
|
|
1132
|
+
var fam = FONT_FAMILIES[familyKey];
|
|
1133
|
+
var mono = FONT_FAMILIES.courier;
|
|
1134
|
+
|
|
1135
|
+
// Font objects: four faces of the body family plus four of Courier, each
|
|
1136
|
+
// carrying the widths this layout was computed with.
|
|
1137
|
+
var fontIds = {}, fontRes = [];
|
|
1138
|
+
function addFont(name, key, styleIndex, alias) {
|
|
1139
|
+
var widths = widthsFor(key, styleIndex);
|
|
1140
|
+
var id = pdf.add("<< /Type /Font /Subtype /Type1 /BaseFont /" + name +
|
|
1141
|
+
" /Encoding /WinAnsiEncoding /FirstChar " + FIRST_CHAR + " /LastChar " + LAST_CHAR +
|
|
1142
|
+
" /Widths [" + widths.join(" ") + "] >>");
|
|
1143
|
+
fontIds[alias] = id;
|
|
1144
|
+
fontRes.push("/" + alias + " " + id + " 0 R");
|
|
1145
|
+
}
|
|
1146
|
+
for (var s = 0; s < 4; s++) {
|
|
1147
|
+
addFont(fam.faces[s], familyKey, s, "F" + s);
|
|
1148
|
+
addFont(mono.faces[s], "courier", s, "M" + s);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
var images = {}, imageRes = [];
|
|
1152
|
+
function imageRef(img) {
|
|
1153
|
+
var src = img.getAttribute("src") || "";
|
|
1154
|
+
if (images[src]) return images[src];
|
|
1155
|
+
var data = encodeImage(img);
|
|
1156
|
+
if (!data) return null;
|
|
1157
|
+
var id = pdf.add({
|
|
1158
|
+
dict: "<< /Type /XObject /Subtype /Image /Width " + data.width + " /Height " + data.height +
|
|
1159
|
+
" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length 0 >>",
|
|
1160
|
+
stream: data.bytes
|
|
1161
|
+
});
|
|
1162
|
+
var alias = "Im" + (imageRes.length + 1);
|
|
1163
|
+
images[src] = { id: id, alias: alias };
|
|
1164
|
+
imageRes.push("/" + alias + " " + id + " 0 R");
|
|
1165
|
+
return images[src];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
var pageIds = [], contentIds = [], annotIds = [];
|
|
1169
|
+
for (var p = 0; p < pages.length; p++) {
|
|
1170
|
+
pageIds.push(pdf.alloc());
|
|
1171
|
+
contentIds.push(pdf.alloc());
|
|
1172
|
+
annotIds.push([]);
|
|
1173
|
+
}
|
|
1174
|
+
var pagesId = pdf.alloc();
|
|
1175
|
+
|
|
1176
|
+
var allMarks = [], heads = [], linkAnnots = [];
|
|
1177
|
+
for (var pi = 0; pi < pages.length; pi++) {
|
|
1178
|
+
var tagger = new Tagger(pi);
|
|
1179
|
+
var draw = renderPage(pages[pi], geom, familyKey, baseSize, imageRef, pi + 1, pages.length, options, tagger);
|
|
1180
|
+
for (var hd = 0; hd < (draw.headings || []).length; hd++) { draw.headings[hd].page = pi; heads.push(draw.headings[hd]); }
|
|
1181
|
+
pdf.set(contentIds[pi], { dict: "<< /Length 0 >>", stream: draw.content });
|
|
1182
|
+
allMarks.push(draw.marks);
|
|
1183
|
+
for (var a = 0; a < draw.links.length; a++) {
|
|
1184
|
+
var lk = draw.links[a];
|
|
1185
|
+
// 2026-10-15 each link annotation is in the tag tree: /StructParent (keys after the pages'), /Contents
|
|
1186
|
+
// naming it, and a Link element built in buildStructTree. They were click targets a screen reader
|
|
1187
|
+
// could not find (PDF/UA 7.18, Matterhorn 28-011 / 28-004). tools/pdf/link-tags.mjs
|
|
1188
|
+
var lsp = pages.length + linkAnnots.length;
|
|
1189
|
+
var lid = pdf.add("<< /Type /Annot /Subtype /Link /Rect [" +
|
|
1190
|
+
fixed(lk.x0) + " " + fixed(lk.y0) + " " + fixed(lk.x1) + " " + fixed(lk.y1) +
|
|
1191
|
+
"] /Border [0 0 0] /StructParent " + lsp + " /Contents " + pdfText(String(lk.text || lk.href).replace(/\s+/g, " ").trim() || lk.href) +
|
|
1192
|
+
" /A << /S /URI /URI (" + escapeString(pdfUri(lk.href)) + ") >> >>");
|
|
1193
|
+
annotIds[pi].push(lid);
|
|
1194
|
+
linkAnnots.push({ id: lid, page: pi, key: lsp, owner: lk.owner });
|
|
1195
|
+
}
|
|
1196
|
+
for (var cm = 0; cm < (draw.comments || []).length; cm++) {
|
|
1197
|
+
var cmt = draw.comments[cm];
|
|
1198
|
+
// 2026-10-21 a note is tagged like a link (PDF/UA 7.18, the rule the link tags already follow): it has a
|
|
1199
|
+
// /StructParent key, a Note element names it, and the parent tree maps the key to that element. tools/pdf/comments.mjs
|
|
1200
|
+
var noteKey = pages.length + linkAnnots.length;
|
|
1201
|
+
var noteId = pdf.add("<< /Type /Annot /Subtype /Text /Rect [" + fixed(cmt.x0) + " " + fixed(cmt.y0) + " " + fixed(cmt.x1) + " " + fixed(cmt.y1) + "]" +
|
|
1202
|
+
" /Contents " + pdfText(cmt.text) + " /T " + pdfText(cmt.author) + " /Name /Note /Open false /C [1 1 0] /StructParent " + noteKey + " >>");
|
|
1203
|
+
annotIds[pi].push(noteId);
|
|
1204
|
+
linkAnnots.push({ id: noteId, page: pi, key: noteKey, owner: null, note: true });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (activeUni && Object.keys(activeUni.used).length) fontRes.push("/U0 " + addUnicodeFont(pdf, activeUni) + " 0 R");
|
|
1209
|
+
|
|
1210
|
+
var struct = buildStructTree(pdf, allMarks, pageIds, linkAnnots);
|
|
1211
|
+
|
|
1212
|
+
for (var pj = 0; pj < pages.length; pj++) {
|
|
1213
|
+
pdf.set(pageIds[pj], "<< /Type /Page /Parent " + pagesId + " 0 R /MediaBox [0 0 " +
|
|
1214
|
+
fixed(geom.width) + " " + fixed(geom.height) + "] /Resources << /Font << " + fontRes.join(" ") + " >>" +
|
|
1215
|
+
(imageRes.length ? " /XObject << " + imageRes.join(" ") + " >>" : "") +
|
|
1216
|
+
" >> /Contents " + contentIds[pj] + " 0 R /StructParents " + pj +
|
|
1217
|
+
(annotIds[pj].length ? " /Annots [" + annotIds[pj].map(function (id) { return id + " 0 R"; }).join(" ") + "] /Tabs /S" : "") +
|
|
1218
|
+
" >>");
|
|
1219
|
+
}
|
|
1220
|
+
pdf.set(pagesId, "<< /Type /Pages /Count " + pages.length + " /Kids [" +
|
|
1221
|
+
pageIds.map(function (id) { return id + " 0 R"; }).join(" ") + "] >>");
|
|
1222
|
+
|
|
1223
|
+
// 2026-10-15 bookmarks from the headings, nested by level, each opening its heading's page at the heading
|
|
1224
|
+
// (Word and Chrome export them; a long report had nothing but scrolling - WCAG 2.4.5, PDF2).
|
|
1225
|
+
// tools/pdf/outline-bookmarks.mjs
|
|
1226
|
+
var outlineId = 0;
|
|
1227
|
+
if (heads.length) {
|
|
1228
|
+
outlineId = pdf.alloc();
|
|
1229
|
+
var oroot = { id: outlineId, kids: [] }, ostack = [oroot], onodes = [];
|
|
1230
|
+
for (var oh = 0; oh < heads.length; oh++) {
|
|
1231
|
+
var on = { h: heads[oh], id: pdf.alloc(), kids: [], parent: null };
|
|
1232
|
+
while (ostack.length > 1 && ostack[ostack.length - 1].h.level >= on.h.level) ostack.pop();
|
|
1233
|
+
on.parent = ostack[ostack.length - 1];
|
|
1234
|
+
on.parent.kids.push(on);
|
|
1235
|
+
ostack.push(on);
|
|
1236
|
+
onodes.push(on);
|
|
1237
|
+
}
|
|
1238
|
+
var ocount = function (n) { var c = n.kids.length; for (var k = 0; k < n.kids.length; k++) c += ocount(n.kids[k]); return c; };
|
|
1239
|
+
for (var oi = 0; oi < onodes.length; oi++) {
|
|
1240
|
+
var n = onodes[oi], sib = n.parent.kids, at = sib.indexOf(n);
|
|
1241
|
+
pdf.set(n.id, "<< /Title " + pdfText(n.h.title) + " /Parent " + n.parent.id + " 0 R" +
|
|
1242
|
+
(at > 0 ? " /Prev " + sib[at - 1].id + " 0 R" : "") + (at < sib.length - 1 ? " /Next " + sib[at + 1].id + " 0 R" : "") +
|
|
1243
|
+
(n.kids.length ? " /First " + n.kids[0].id + " 0 R /Last " + n.kids[n.kids.length - 1].id + " 0 R /Count " + ocount(n) : "") +
|
|
1244
|
+
" /Dest [" + pageIds[n.h.page] + " 0 R /XYZ null " + fixed(n.h.y) + " null] >>");
|
|
1245
|
+
}
|
|
1246
|
+
pdf.set(outlineId, "<< /Type /Outlines /First " + oroot.kids[0].id + " 0 R /Last " + oroot.kids[oroot.kids.length - 1].id + " 0 R /Count " + ocount(oroot) + " >>");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
var title = options.title || config.pdfExportTitle || document.title || "Document";
|
|
1250
|
+
var author = options.author || config.pdfExportAuthor || "";
|
|
1251
|
+
// /MarkInfo and /StructTreeRoot declare the file tagged. DisplayDocTitle
|
|
1252
|
+
// makes the viewer show the document's title rather than its filename,
|
|
1253
|
+
// which PDF/UA requires and which is the difference between a screen
|
|
1254
|
+
// reader announcing "Quarterly Report" and "Export-260802-1431.pdf".
|
|
1255
|
+
var catalogId = pdf.add("<< /Type /Catalog /Pages " + pagesId + " 0 R" +
|
|
1256
|
+
" /Lang (" + escapeString(String(options.lang || document.documentElement.lang || "en")) + ")" +
|
|
1257
|
+
" /MarkInfo << /Marked true >>" +
|
|
1258
|
+
(outlineId ? " /Outlines " + outlineId + " 0 R" : "") +
|
|
1259
|
+
" /StructTreeRoot " + struct.rootId + " 0 R" +
|
|
1260
|
+
" /ViewerPreferences << /DisplayDocTitle true >> >>");
|
|
1261
|
+
var infoId = pdf.add("<< /Title " + pdfText(String(title)) +
|
|
1262
|
+
(author ? " /Author " + pdfText(String(author)) : "") +
|
|
1263
|
+
" /Producer (RichTextEditor) /Creator (RichTextEditor) /CreationDate (" + pdfDate(new Date()) + ") >>");
|
|
1264
|
+
|
|
1265
|
+
return pdf.build(catalogId, infoId);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Turn the per-page marked-content records into a structure tree.
|
|
1269
|
+
//
|
|
1270
|
+
// The tree is built from the marks rather than from the block list because a
|
|
1271
|
+
// single block can be split across pages: its structure element then holds
|
|
1272
|
+
// several marked-content references, each naming the page it lives on.
|
|
1273
|
+
function buildStructTree(pdf, allMarks, pageIds, linkAnnots) {
|
|
1274
|
+
var rootId = pdf.alloc();
|
|
1275
|
+
var nodes = []; // { id, role, kids, extra, children, parent }
|
|
1276
|
+
var parentTree = []; // per page: array of struct ids by MCID
|
|
1277
|
+
|
|
1278
|
+
function node(role, extra) {
|
|
1279
|
+
var n = { id: pdf.alloc(), role: role, kids: [], extra: extra || "", children: [], parent: null, seq: [] };
|
|
1280
|
+
nodes.push(n);
|
|
1281
|
+
return n;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
var docNode = node("Document");
|
|
1285
|
+
var listNodes = new Map(); // listId -> L node
|
|
1286
|
+
var itemNodes = new Map(); // block -> LI node
|
|
1287
|
+
var labelNodes = new Map(); // block -> Lbl node
|
|
1288
|
+
var bodyNodes = new Map(); // block -> P / LBody / Figure node
|
|
1289
|
+
var tableNodes = new Map(); // block -> Table node
|
|
1290
|
+
var rowNodes = new Map(); // row -> TR node
|
|
1291
|
+
var cellNodes = new Map(); // row -> { col -> TD/TH node }
|
|
1292
|
+
var linkNodes = new Map(); // link owner -> Link node (2026-10-17, tools/pdf/link-order.mjs)
|
|
1293
|
+
|
|
1294
|
+
for (var p = 0; p < allMarks.length; p++) {
|
|
1295
|
+
var marks = allMarks[p];
|
|
1296
|
+
parentTree[p] = [];
|
|
1297
|
+
for (var m = 0; m < marks.length; m++) {
|
|
1298
|
+
var owner = marks[m].owner, mcid = marks[m].mcid;
|
|
1299
|
+
var target = resolve(owner);
|
|
1300
|
+
target.kids.push({ page: p, mcid: mcid });
|
|
1301
|
+
target.seq.push({ mcr: { page: p, mcid: mcid } });
|
|
1302
|
+
parentTree[p][mcid] = target.id;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// The parent is recorded when the child is attached. Searching for it
|
|
1307
|
+
// afterwards would be a scan of every node for every node.
|
|
1308
|
+
function attach(parent, child) {
|
|
1309
|
+
if (child.parent) return;
|
|
1310
|
+
child.parent = parent;
|
|
1311
|
+
parent.children.push(child);
|
|
1312
|
+
parent.seq.push({ elem: child });
|
|
1313
|
+
}
|
|
1314
|
+
// a Link element per link annotation, under the paragraph / cell holding it
|
|
1315
|
+
var linkKeys = [];
|
|
1316
|
+
for (var la = 0; la < (linkAnnots || []).length; la++) {
|
|
1317
|
+
if (linkAnnots[la].note) {
|
|
1318
|
+
// a comment note: a Note element under the document, its OBJR and its parent-tree key
|
|
1319
|
+
var nte = node("Note");
|
|
1320
|
+
attach(docNode, nte);
|
|
1321
|
+
nte.objr = linkAnnots[la];
|
|
1322
|
+
linkKeys.push(linkAnnots[la].key + " " + nte.id + " 0 R");
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
var lnk;
|
|
1326
|
+
if (linkAnnots[la].owner && linkAnnots[la].owner.kind === "link") lnk = resolve(linkAnnots[la].owner);
|
|
1327
|
+
else { lnk = node("Link"); attach(resolve(linkAnnots[la].owner), lnk); }
|
|
1328
|
+
lnk.objr = linkAnnots[la];
|
|
1329
|
+
linkKeys.push(linkAnnots[la].key + " " + lnk.id + " 0 R");
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function resolve(owner) {
|
|
1333
|
+
// 2026-10-17 a link's words: their own Link element under the paragraph or cell, created where they occur
|
|
1334
|
+
if (owner.kind === "link") {
|
|
1335
|
+
var ln = linkNodes.get(owner);
|
|
1336
|
+
if (!ln) { ln = node("Link"); linkNodes.set(owner, ln); attach(resolve(owner.parent), ln); }
|
|
1337
|
+
return ln;
|
|
1338
|
+
}
|
|
1339
|
+
if (owner.kind === "cell") {
|
|
1340
|
+
var table = tableNodes.get(owner.block);
|
|
1341
|
+
if (!table) { table = node("Table"); tableNodes.set(owner.block, table); attach(docNode, table); }
|
|
1342
|
+
var row = rowNodes.get(owner.row);
|
|
1343
|
+
if (!row) { row = node("TR"); rowNodes.set(owner.row, row); attach(table, row); }
|
|
1344
|
+
var perRow = cellNodes.get(owner.row);
|
|
1345
|
+
if (!perRow) { perRow = {}; cellNodes.set(owner.row, perRow); }
|
|
1346
|
+
if (!perRow[owner.col]) {
|
|
1347
|
+
// A header cell must be TH, not TD: it is what lets a screen
|
|
1348
|
+
// reader announce "Revenue, 1,240" instead of just "1,240".
|
|
1349
|
+
// 2026-10-15 table attributes belong in /A << /O /Table >> (where PDF/UA checkers read them; /Scope
|
|
1350
|
+
// sat on the element), with /ColSpan for a merged cell. tools/pdf/table-colspan.mjs
|
|
1351
|
+
var tattr = (owner.header ? " /Scope /Column" : "") + (owner.span > 1 ? " /ColSpan " + owner.span : "") + (owner.rowspan > 1 ? " /RowSpan " + owner.rowspan : "");
|
|
1352
|
+
perRow[owner.col] = node(owner.header ? "TH" : "TD", tattr ? " /A << /O /Table" + tattr + " >>" : "");
|
|
1353
|
+
attach(row, perRow[owner.col]);
|
|
1354
|
+
}
|
|
1355
|
+
return perRow[owner.col];
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
var block = owner.block;
|
|
1359
|
+
var container = docNode;
|
|
1360
|
+
if (block.listId) {
|
|
1361
|
+
var listNode = listNodes.get(block.listId);
|
|
1362
|
+
if (!listNode) { listNode = node("L"); listNodes.set(block.listId, listNode); attach(docNode, listNode); }
|
|
1363
|
+
var itemNode = itemNodes.get(block);
|
|
1364
|
+
if (!itemNode) { itemNode = node("LI"); itemNodes.set(block, itemNode); attach(listNode, itemNode); }
|
|
1365
|
+
container = itemNode;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (owner.kind === "lbl") {
|
|
1369
|
+
var lbl = labelNodes.get(block);
|
|
1370
|
+
if (!lbl) { lbl = node("Lbl"); labelNodes.set(block, lbl); attach(container, lbl); }
|
|
1371
|
+
return lbl;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
var body = bodyNodes.get(block);
|
|
1375
|
+
if (!body) {
|
|
1376
|
+
if (block.listId) {
|
|
1377
|
+
body = node("LBody");
|
|
1378
|
+
} else {
|
|
1379
|
+
// A figure with no alt text is an accessibility failure, but
|
|
1380
|
+
// an EMPTY /Alt is a worse one: it tells assistive tech the
|
|
1381
|
+
// image is decorative when nobody decided that.
|
|
1382
|
+
var extra = block.role === "Figure"
|
|
1383
|
+
? " /Alt " + pdfText(block.alt || "Image")
|
|
1384
|
+
: "";
|
|
1385
|
+
// 2026-10-15 a block in another language says so, or it is read with the document's
|
|
1386
|
+
// pronunciation (WCAG 3.1.2, PDF/UA 7.2). tools/pdf/paragraph-lang.mjs
|
|
1387
|
+
var docLang = pdfDocLang;
|
|
1388
|
+
var bl = String(block.lang || "").replace(/[^A-Za-z0-9-]/g, "");
|
|
1389
|
+
if (bl && bl.toLowerCase() !== docLang) extra += " /Lang (" + bl + ")";
|
|
1390
|
+
body = node(block.role || "P", extra);
|
|
1391
|
+
}
|
|
1392
|
+
bodyNodes.set(block, body);
|
|
1393
|
+
attach(container, body);
|
|
1394
|
+
}
|
|
1395
|
+
return body;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Emit. /K lists child elements and marked-content references in reading order (2026-10-17: child elements
|
|
1399
|
+
// used to come first, so a Link sat at the start of its paragraph). tools/pdf/link-order.mjs
|
|
1400
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
1401
|
+
var nd = nodes[n];
|
|
1402
|
+
var kids = [];
|
|
1403
|
+
for (var k = 0; k < nd.seq.length; k++) {
|
|
1404
|
+
var sq = nd.seq[k];
|
|
1405
|
+
if (sq.elem) kids.push(sq.elem.id + " 0 R");
|
|
1406
|
+
else kids.push("<< /Type /MCR /Pg " + pageIds[sq.mcr.page] + " 0 R /MCID " + sq.mcr.mcid + " >>");
|
|
1407
|
+
}
|
|
1408
|
+
if (nd.objr) kids.push("<< /Type /OBJR /Obj " + nd.objr.id + " 0 R /Pg " + pageIds[nd.objr.page] + " 0 R >>");
|
|
1409
|
+
var parentRef = nd.parent ? nd.parent.id : rootId;
|
|
1410
|
+
pdf.set(nd.id, "<< /Type /StructElem /S /" + nd.role + " /P " + parentRef + " 0 R" + nd.extra +
|
|
1411
|
+
(kids.length ? " /K [" + kids.join(" ") + "]" : "") + " >>");
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// ParentTree: a number tree keyed by each page's /StructParents value,
|
|
1415
|
+
// whose value is the list of owning structure elements indexed by MCID.
|
|
1416
|
+
var numsParts = [];
|
|
1417
|
+
for (var pt = 0; pt < parentTree.length; pt++) {
|
|
1418
|
+
var arr = parentTree[pt] || [];
|
|
1419
|
+
var refs = [];
|
|
1420
|
+
for (var q = 0; q < arr.length; q++) refs.push((arr[q] || docNode.id) + " 0 R");
|
|
1421
|
+
numsParts.push(pt + " [" + refs.join(" ") + "]");
|
|
1422
|
+
}
|
|
1423
|
+
var parentTreeId = pdf.add("<< /Nums [" + numsParts.concat(linkKeys).join(" ") + "] >>");
|
|
1424
|
+
|
|
1425
|
+
pdf.set(rootId, "<< /Type /StructTreeRoot /K [" + docNode.id + " 0 R]" +
|
|
1426
|
+
" /ParentTree " + parentTreeId + " 0 R /ParentTreeNextKey " + (parentTree.length + linkKeys.length) + " >>");
|
|
1427
|
+
|
|
1428
|
+
return { rootId: rootId };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function pdfDate(d) {
|
|
1432
|
+
function two(n) { return (n < 10 ? "0" : "") + n; }
|
|
1433
|
+
return "D:" + d.getFullYear() + two(d.getMonth() + 1) + two(d.getDate()) +
|
|
1434
|
+
two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds()) + "Z";
|
|
1435
|
+
}
|
|
1436
|
+
function fixed(n) { return (Math.round(n * 100) / 100).toString(); }
|
|
1437
|
+
|
|
1438
|
+
// Marked content for the structure tree.
|
|
1439
|
+
//
|
|
1440
|
+
// Tagging is what separates "a screen reader can read the words" from "a
|
|
1441
|
+
// screen reader knows this is a level-2 heading, followed by a 4-item list,
|
|
1442
|
+
// followed by a table with header cells". Section 508 and EN 301 549 both
|
|
1443
|
+
// require the second. Purely decorative output (rules, cell borders, the
|
|
1444
|
+
// running header) is marked as an Artifact instead, which removes it from
|
|
1445
|
+
// the reading order rather than reading "line, line, line" to the user.
|
|
1446
|
+
function Tagger(pageIndex) {
|
|
1447
|
+
this.pageIndex = pageIndex;
|
|
1448
|
+
this.next = 0;
|
|
1449
|
+
this.marks = []; // { owner, mcid } in reading order
|
|
1450
|
+
}
|
|
1451
|
+
Tagger.prototype.open = function (out, role, owner) {
|
|
1452
|
+
var mcid = this.next++;
|
|
1453
|
+
this.marks.push({ owner: owner, mcid: mcid, role: role });
|
|
1454
|
+
out.push("/" + role + " << /MCID " + mcid + " >> BDC");
|
|
1455
|
+
return mcid;
|
|
1456
|
+
};
|
|
1457
|
+
Tagger.prototype.close = function (out) { out.push("EMC"); };
|
|
1458
|
+
Tagger.prototype.artifact = function (out, fn) {
|
|
1459
|
+
out.push("/Artifact BMC");
|
|
1460
|
+
fn();
|
|
1461
|
+
out.push("EMC");
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
function renderPage(ops, geom, familyKey, baseSize, imageRef, pageNumber, pageCount, options, tagger) {
|
|
1465
|
+
var out = [], links = [], headings = []; links.comments = []; links.seenComments = {};
|
|
1466
|
+
var left = geom.margin.left;
|
|
1467
|
+
|
|
1468
|
+
for (var i = 0; i < ops.length; i++) {
|
|
1469
|
+
var op = ops[i];
|
|
1470
|
+
// 2026-10-15 where each heading starts, for the bookmarks. tools/pdf/outline-bookmarks.mjs
|
|
1471
|
+
if (op.op === "line" && op.first && op.block && /^H[1-6]$/.test(op.block.role || "")) {
|
|
1472
|
+
var htext = (op.block.lines || []).map(function (ln) { return (ln.parts || []).map(function (pt) { return pt.text || ""; }).join(""); }).join(" ").replace(/\s+/g, " ").trim();
|
|
1473
|
+
if (htext) headings.push({ level: +op.block.role.charAt(1), title: htext, y: op.y });
|
|
1474
|
+
}
|
|
1475
|
+
if (op.op === "rule") {
|
|
1476
|
+
// A horizontal rule is decoration, not content.
|
|
1477
|
+
tagger.artifact(out, function () {
|
|
1478
|
+
out.push("0.8 0.8 0.8 RG 0.5 w " + fixed(left + op.indent) + " " + fixed(op.y) + " m " +
|
|
1479
|
+
fixed(geom.width - geom.margin.right) + " " + fixed(op.y) + " l S");
|
|
1480
|
+
});
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
if (op.op === "row") { renderRow(op, geom, out, left, imageRef, links, tagger); continue; }
|
|
1484
|
+
renderLine(op, geom, out, left, familyKey, imageRef, links, tagger);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Running header/footer, drawn after the body so they always sit on top.
|
|
1488
|
+
// Both are artifacts: repeating them into the reading order on every
|
|
1489
|
+
// page is one of the most common findings in a PDF accessibility audit.
|
|
1490
|
+
var hf = headerFooter(options);
|
|
1491
|
+
if (hf.header || hf.footer) {
|
|
1492
|
+
tagger.artifact(out, function () {
|
|
1493
|
+
if (hf.header) drawRunning(out, hf.header, geom, geom.height - geom.margin.top * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
|
|
1494
|
+
if (hf.footer) drawRunning(out, hf.footer, geom, geom.margin.bottom * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return { content: out.join("\n"), links: links, marks: tagger.marks, headings: headings, comments: links.comments };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function headerFooter(options) {
|
|
1502
|
+
var setup = null;
|
|
1503
|
+
try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); } catch (e) {}
|
|
1504
|
+
setup = setup || {};
|
|
1505
|
+
return {
|
|
1506
|
+
header: options.headerText || config.pdfExportHeader || plainText(setup.headerHtml) || "",
|
|
1507
|
+
footer: options.footerText || config.pdfExportFooter || plainText(setup.footerHtml) || ""
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
function plainText(html) {
|
|
1511
|
+
if (!html || typeof html !== "string") return "";
|
|
1512
|
+
var d = document.createElement("div");
|
|
1513
|
+
d.innerHTML = __RTE_TT(html);
|
|
1514
|
+
return (d.textContent || "").replace(/\s+/g, " ").trim();
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function drawRunning(out, template, geom, y, familyKey, size, pageNumber, pageCount) {
|
|
1518
|
+
var text = String(template).replace(/\{page\}/g, pageNumber).replace(/\{total\}/g, pageCount);
|
|
1519
|
+
var win = toWinAnsi(text);
|
|
1520
|
+
var w = textWidth(win, familyKey, 0, size);
|
|
1521
|
+
var x = (geom.width - w) / 2;
|
|
1522
|
+
out.push("BT /F0 " + fixed(size) + " Tf 0.4 0.4 0.4 rg " + fixed(x) + " " + fixed(y) + " Td (" + escapeString(win) + ") Tj ET");
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function renderLine(op, geom, out, left, familyKey, imageRef, links, tagger) {
|
|
1526
|
+
var line = op.line, block = op.block;
|
|
1527
|
+
var x = left + (block.indent || 0);
|
|
1528
|
+
var avail = geom.width - geom.margin.right - x;
|
|
1529
|
+
if (block.align === "center") x += (avail - line.width) / 2;
|
|
1530
|
+
else if (block.align === "right") x += avail - line.width;
|
|
1531
|
+
|
|
1532
|
+
if (block.rule === "quote" && !line.image) {
|
|
1533
|
+
// The quote bar is decoration.
|
|
1534
|
+
var barX = fixed(left + block.indent - 8);
|
|
1535
|
+
var run = function () {
|
|
1536
|
+
out.push("0.8 0.8 0.85 RG 2 w " + barX + " " + fixed(op.y - line.height + 4) +
|
|
1537
|
+
" m " + barX + " " + fixed(op.y + line.size * 0.8) + " l S");
|
|
1538
|
+
};
|
|
1539
|
+
if (tagger) tagger.artifact(out, run); else run();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// A list marker belongs to the FIRST line only; repeating it on a wrapped
|
|
1543
|
+
// continuation line turns one bullet into several.
|
|
1544
|
+
if (op.first && block.marker) {
|
|
1545
|
+
var ms = block.markerStyle || { size: line.size, color: [0, 0, 0] };
|
|
1546
|
+
var mw = textWidth(block.marker, familyKey, 0, ms.size);
|
|
1547
|
+
if (tagger) tagger.open(out, "Lbl", { kind: "lbl", block: block });
|
|
1548
|
+
out.push("BT /F0 " + fixed(ms.size) + " Tf " + colorOp(ms.color) + " " +
|
|
1549
|
+
fixed(x - mw - 6) + " " + fixed(op.y - line.size * 0.85) + " Td (" + escapeString(toWinAnsi(block.marker)) + ") Tj ET");
|
|
1550
|
+
if (tagger) tagger.close(out);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Text of the block itself. Figures carry /Alt on the structure element
|
|
1554
|
+
// rather than in the content stream.
|
|
1555
|
+
//
|
|
1556
|
+
// A table cell renders through this function with a SYNTHETIC block
|
|
1557
|
+
// (just indent/align), so the owner has to be the cell descriptor the
|
|
1558
|
+
// caller supplied. Passing the synthetic block instead makes every cell
|
|
1559
|
+
// a top-level paragraph and the table loses its structure entirely.
|
|
1560
|
+
var lineOwner = op.cell || { kind: "body", block: block };
|
|
1561
|
+
var lineOpen = false;
|
|
1562
|
+
if (tagger) { tagger.open(out, line.image ? "Figure" : "Span", lineOwner); lineOpen = true; }
|
|
1563
|
+
|
|
1564
|
+
// Wrapping split the text into words, but emitting one BT/Tj block per
|
|
1565
|
+
// WORD is wrong twice over: the content stream balloons, and text
|
|
1566
|
+
// extractors that infer spacing from separate show-operations can lose
|
|
1567
|
+
// the spaces between them, so copying a paragraph out of the PDF yields
|
|
1568
|
+
// "onelongrunofwords". Adjacent parts that share a font, size and colour
|
|
1569
|
+
// are merged back into a single run, spaces included.
|
|
1570
|
+
var segments = [];
|
|
1571
|
+
var cursor = x;
|
|
1572
|
+
for (var i = 0; i < line.parts.length; i++) {
|
|
1573
|
+
var part = line.parts[i];
|
|
1574
|
+
if (part.img) { segments.push({ img: part, x: cursor, w: part.w }); cursor += part.w; continue; }
|
|
1575
|
+
var st = part.style;
|
|
1576
|
+
var alias = part.uni ? "U0" : (st.mono ? "M" : "F") + part.styleIndex;
|
|
1577
|
+
var last = segments.length ? segments[segments.length - 1] : null;
|
|
1578
|
+
var mergeable = last && !last.img && last.alias === alias && last.style.size === st.size &&
|
|
1579
|
+
sameColor(last.style.color, st.color) && last.style.underline === st.underline &&
|
|
1580
|
+
last.style.strike === st.strike && last.style.link === st.link && (last.style.lang || "") === (st.lang || "") &&
|
|
1581
|
+
// 2026-10-21 a commented run is its own segment: merged into its neighbours it lost its note (tools/pdf/comments.mjs)
|
|
1582
|
+
(last.style.comment ? last.style.comment.text + "|" + last.style.comment.author : "") === (st.comment ? st.comment.text + "|" + st.comment.author : "");
|
|
1583
|
+
if (mergeable) { last.text += part.text; last.w += part.w; }
|
|
1584
|
+
else { segments.push({ text: part.text, style: st, alias: alias, x: cursor, w: part.w }); }
|
|
1585
|
+
cursor += part.w;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
var baseline = op.y - line.size * 0.85;
|
|
1589
|
+
for (var s = 0; s < segments.length; s++) {
|
|
1590
|
+
var seg = segments[s];
|
|
1591
|
+
if (seg.img) {
|
|
1592
|
+
var ref = imageRef(seg.img.img);
|
|
1593
|
+
if (ref) {
|
|
1594
|
+
out.push("q " + fixed(seg.w) + " 0 0 " + fixed(seg.img.h) + " " + fixed(seg.x) + " " +
|
|
1595
|
+
fixed(op.y - seg.img.h) + " cm /" + ref.alias + " Do Q");
|
|
1596
|
+
}
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
// A run of pure whitespace still advanced the cursor; drawing it
|
|
1600
|
+
// would only add an empty operation.
|
|
1601
|
+
if (!seg.text || /^\s+$/.test(seg.text)) continue;
|
|
1602
|
+
var sst = seg.style;
|
|
1603
|
+
// 2026-10-17 a link's words are marked content of their own, owned by the Link element, so the Link holds
|
|
1604
|
+
// its text at its place in the paragraph (PDF/UA 7.18.5). tools/pdf/link-order.mjs
|
|
1605
|
+
var linkOwner = null;
|
|
1606
|
+
if (tagger && !line.image && sst.link) {
|
|
1607
|
+
if (lineOpen) { tagger.close(out); lineOpen = false; }
|
|
1608
|
+
linkOwner = { kind: "link", parent: lineOwner };
|
|
1609
|
+
tagger.open(out, "Span", linkOwner);
|
|
1610
|
+
} else if (tagger && !lineOpen) {
|
|
1611
|
+
tagger.open(out, "Span", lineOwner); lineOpen = true;
|
|
1612
|
+
}
|
|
1613
|
+
// 2026-10-17 a run in another language than its block (a French phrase in an English sentence) is drawn
|
|
1614
|
+
// inside marked content with a /Lang property, so it is read with the right pronunciation (WCAG 3.1.2,
|
|
1615
|
+
// PDF/UA 7.2). The paragraph's own /Lang is on its structure element. tools/pdf/inline-lang.mjs
|
|
1616
|
+
var runLang = String(sst.lang || "").replace(/[^A-Za-z0-9-]/g, "");
|
|
1617
|
+
var langMark = runLang && runLang.toLowerCase() !== String(block.lang || pdfDocLang || "").toLowerCase();
|
|
1618
|
+
if (langMark) out.push("/Span << /Lang (" + runLang + ") >> BDC");
|
|
1619
|
+
if (seg.alias === "U0") {
|
|
1620
|
+
out.push("BT /U0 " + fixed(sst.size) + " Tf " + colorOp(sst.color) +
|
|
1621
|
+
(sst.bold ? " " + colorStroke(sst.color) + " 2 Tr " + fixed(sst.size * 0.03) + " w" : "") + " " +
|
|
1622
|
+
(sst.italic ? "1 0 0.21 1 " + fixed(seg.x) + " " + fixed(baseline) + " Tm " : fixed(seg.x) + " " + fixed(baseline) + " Td ") +
|
|
1623
|
+
// the render mode outlives ET: reset it, or every later run is stroked too
|
|
1624
|
+
uniHex(seg.text) + " Tj" + (sst.bold ? " 0 Tr" : "") + " ET");
|
|
1625
|
+
} else {
|
|
1626
|
+
out.push("BT /" + seg.alias + " " + fixed(sst.size) + " Tf " + colorOp(sst.color) + " " +
|
|
1627
|
+
fixed(seg.x) + " " + fixed(baseline) + " Td (" + escapeString(toWinAnsi(seg.text)) + ") Tj ET");
|
|
1628
|
+
}
|
|
1629
|
+
if (langMark) out.push("EMC");
|
|
1630
|
+
if (sst.underline) {
|
|
1631
|
+
out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline - sst.size * 0.12) +
|
|
1632
|
+
" m " + fixed(seg.x + seg.w) + " " + fixed(baseline - sst.size * 0.12) + " l S");
|
|
1633
|
+
}
|
|
1634
|
+
if (sst.strike) {
|
|
1635
|
+
out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline + sst.size * 0.28) +
|
|
1636
|
+
" m " + fixed(seg.x + seg.w) + " " + fixed(baseline + sst.size * 0.28) + " l S");
|
|
1637
|
+
}
|
|
1638
|
+
if (sst.comment) {
|
|
1639
|
+
var cmKey = sst.comment.text + "|" + sst.comment.author;
|
|
1640
|
+
if (!links.seenComments[cmKey]) { links.seenComments[cmKey] = true; links.comments.push({ x0: seg.x, y0: baseline - 2, x1: seg.x + 12, y1: baseline + 10, text: sst.comment.text, author: sst.comment.author }); }
|
|
1641
|
+
}
|
|
1642
|
+
if (sst.link) {
|
|
1643
|
+
links.push({ href: sst.link, x0: seg.x, y0: baseline - 2, x1: seg.x + seg.w, y1: baseline + sst.size, text: seg.text || "", owner: linkOwner || lineOwner });
|
|
1644
|
+
}
|
|
1645
|
+
if (linkOwner) tagger.close(out);
|
|
1646
|
+
}
|
|
1647
|
+
if (tagger && lineOpen) tagger.close(out);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function sameColor(a, b) {
|
|
1651
|
+
a = a || [0, 0, 0]; b = b || [0, 0, 0];
|
|
1652
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function renderRow(op, geom, out, left, imageRef, links, tagger) {
|
|
1656
|
+
var block = op.block, row = op.row;
|
|
1657
|
+
var x = left + block.indent;
|
|
1658
|
+
var top = op.y, bottom = op.y - row.height;
|
|
1659
|
+
|
|
1660
|
+
for (var c = 0; c < row.cells.length; c++) {
|
|
1661
|
+
var cspan = row.cells[c].span || 1, cstart = row.cells[c].start != null ? row.cells[c].start : c;
|
|
1662
|
+
var cellX = x, cellW = 0;
|
|
1663
|
+
for (var wi = 0; wi < cstart + cspan; wi++) {
|
|
1664
|
+
var colw = block.colWidths && block.colWidths[wi] != null ? block.colWidths[wi] : block.colWidth;
|
|
1665
|
+
if (wi < cstart) cellX += colw; else cellW += colw;
|
|
1666
|
+
}
|
|
1667
|
+
// Shading and borders are decoration; only the cell TEXT belongs to
|
|
1668
|
+
// the reading order.
|
|
1669
|
+
var paint = function () {
|
|
1670
|
+
if (row.header) {
|
|
1671
|
+
out.push("0.94 0.95 0.97 rg " + fixed(cellX) + " " + fixed(bottom) + " " +
|
|
1672
|
+
fixed(cellW) + " " + fixed(row.height) + " re f");
|
|
1673
|
+
}
|
|
1674
|
+
out.push("0.75 0.78 0.82 RG 0.5 w " + fixed(cellX) + " " + fixed(bottom) + " " +
|
|
1675
|
+
fixed(cellW) + " " + fixed(row.height) + " re S");
|
|
1676
|
+
};
|
|
1677
|
+
if (tagger) tagger.artifact(out, paint); else paint();
|
|
1678
|
+
|
|
1679
|
+
// One structure element per CELL, identified by the row object and
|
|
1680
|
+
// column index so a table split across pages still resolves to the
|
|
1681
|
+
// right TD (and a repeated header row to the right TH).
|
|
1682
|
+
var cellOwner = { kind: "cell", block: block, row: row, col: cstart, span: cspan, rowspan: row.cells[c].rowspan || 1, header: !!row.cells[c].header };
|
|
1683
|
+
var cy = top - block.pad;
|
|
1684
|
+
var lines = row.cells[c].lines;
|
|
1685
|
+
for (var l = 0; l < lines.length; l++) {
|
|
1686
|
+
renderLine({ line: lines[l], y: cy, block: { indent: 0, align: "left" }, first: l === 0, cell: cellOwner },
|
|
1687
|
+
{ width: cellX + cellW + geom.margin.right, margin: { right: geom.margin.right } },
|
|
1688
|
+
out, cellX + block.pad, "helvetica", imageRef, links, tagger);
|
|
1689
|
+
cy -= lines[l].height;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function colorOp(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " rg"; }
|
|
1695
|
+
function colorStroke(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " RG"; }
|
|
1696
|
+
|
|
1697
|
+
// Every image is re-encoded to JPEG through a canvas. PNG could be embedded
|
|
1698
|
+
// as FlateDecode without re-encoding, but only for a narrow set of colour
|
|
1699
|
+
// types and never with an alpha channel; routing everything through one path
|
|
1700
|
+
// means GIF, WEBP, BMP and SVG all work rather than only some of them.
|
|
1701
|
+
// Transparent pixels are composited onto white, which is what the page is.
|
|
1702
|
+
function encodeImage(img) {
|
|
1703
|
+
try {
|
|
1704
|
+
var w = img.naturalWidth || img.width, h = img.naturalHeight || img.height;
|
|
1705
|
+
if (!w || !h) return null;
|
|
1706
|
+
var max = 2000;
|
|
1707
|
+
var scale = Math.min(1, max / Math.max(w, h));
|
|
1708
|
+
var cw = Math.max(1, Math.round(w * scale)), ch = Math.max(1, Math.round(h * scale));
|
|
1709
|
+
var canvas = document.createElement("canvas");
|
|
1710
|
+
canvas.width = cw; canvas.height = ch;
|
|
1711
|
+
var g = canvas.getContext("2d");
|
|
1712
|
+
g.fillStyle = "#ffffff";
|
|
1713
|
+
g.fillRect(0, 0, cw, ch);
|
|
1714
|
+
g.drawImage(img, 0, 0, cw, ch);
|
|
1715
|
+
var uri = canvas.toDataURL("image/jpeg", 0.92);
|
|
1716
|
+
var comma = uri.indexOf(",");
|
|
1717
|
+
if (comma < 0) return null;
|
|
1718
|
+
var binary = atob(uri.slice(comma + 1));
|
|
1719
|
+
var bytes = new Uint8Array(binary.length);
|
|
1720
|
+
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1721
|
+
return { bytes: bytes, width: cw, height: ch };
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
// A cross-origin image taints the canvas. Skipping it is correct:
|
|
1724
|
+
// there is no way to read its pixels, and failing the whole export
|
|
1725
|
+
// over one decorative image would be worse.
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// ------------------------------------------------------------- downloads
|
|
1731
|
+
function sanitizeName(name) {
|
|
1732
|
+
if (!name || typeof name !== "string") return "";
|
|
1733
|
+
return name.replace(/\.pdf$/i, "").replace(/[\\/:*?"<>|]+/g, "").trim().slice(0, 120);
|
|
1734
|
+
}
|
|
1735
|
+
function defaultBase() {
|
|
1736
|
+
var d = new Date();
|
|
1737
|
+
function two(n) { return (n < 10 ? "0" : "") + n; }
|
|
1738
|
+
return "Export-" + String(d.getFullYear()).slice(2) + two(d.getMonth() + 1) + two(d.getDate()) +
|
|
1739
|
+
"-" + two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds());
|
|
1740
|
+
}
|
|
1741
|
+
function download(bytes, filename) {
|
|
1742
|
+
var blob = new Blob([bytes], { type: "application/pdf" });
|
|
1743
|
+
var url = URL.createObjectURL(blob);
|
|
1744
|
+
var a = document.createElement("a");
|
|
1745
|
+
a.href = url; a.download = filename;
|
|
1746
|
+
document.body.appendChild(a);
|
|
1747
|
+
a.click();
|
|
1748
|
+
document.body.removeChild(a);
|
|
1749
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
1750
|
+
return filename;
|
|
1751
|
+
}
|
|
1752
|
+
}
|