@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.10
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/LICENSE +21 -0
- package/README.md +35 -21
- package/dist/api/args.d.ts +1 -1
- package/dist/api/args.js +2 -5
- package/dist/api/color.d.ts +4 -4
- package/dist/api/color.js +11 -17
- package/dist/api/content.d.ts +20 -13
- package/dist/api/content.js +44 -29
- package/dist/api/descriptor.d.ts +2 -2
- package/dist/api/descriptor.js +121 -31
- package/dist/api/dimension.d.ts +76 -0
- package/dist/api/dimension.js +43 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +13 -8
- package/dist/api/index.js +13 -24
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +19 -12
- package/dist/api/layout.d.ts +149 -25
- package/dist/api/layout.js +182 -51
- package/dist/api/page-builder.d.ts +40 -0
- package/dist/api/page-builder.js +44 -0
- package/dist/api/structure.d.ts +100 -13
- package/dist/api/structure.js +179 -88
- package/dist/api/table.d.ts +5 -5
- package/dist/api/table.js +38 -26
- package/dist/api/text.d.ts +90 -4
- package/dist/api/text.js +116 -31
- package/dist/assets/font-data.d.ts +2 -0
- package/dist/assets/font-data.js +21 -0
- package/dist/assets/font-data.ts +37 -0
- package/dist/common/color.js +1 -5
- package/dist/constants/page-sizes.js +3 -6
- package/dist/constants/pdf-parts.js +1 -4
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +62 -0
- package/dist/crypto/security-handler.js +155 -0
- package/dist/crypto/webcrypto.d.ts +11 -0
- package/dist/crypto/webcrypto.js +62 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +50 -5
- package/dist/elements/container-element.js +100 -39
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +58 -6
- package/dist/elements/image-element.js +145 -111
- package/dist/elements/index.d.ts +26 -10
- package/dist/elements/index.js +26 -28
- package/dist/elements/layout/anchor-element.d.ts +23 -0
- package/dist/elements/layout/anchor-element.js +27 -0
- package/dist/elements/layout/bookmark-element.d.ts +27 -0
- package/dist/elements/layout/bookmark-element.js +30 -0
- package/dist/elements/layout/default-text-style-element.d.ts +30 -0
- package/dist/elements/layout/default-text-style-element.js +47 -0
- package/dist/elements/layout/deferred-element.d.ts +3 -3
- package/dist/elements/layout/deferred-element.js +4 -8
- package/dist/elements/layout/expanded-element.d.ts +6 -4
- package/dist/elements/layout/expanded-element.js +22 -17
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/padding-element.d.ts +10 -5
- package/dist/elements/layout/padding-element.js +25 -16
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/page-builder-element.d.ts +32 -0
- package/dist/elements/layout/page-builder-element.js +45 -0
- package/dist/elements/layout/positioned-element.d.ts +47 -0
- package/dist/elements/layout/positioned-element.js +73 -0
- package/dist/elements/layout/repeating-header-element.d.ts +3 -3
- package/dist/elements/layout/repeating-header-element.js +8 -12
- package/dist/elements/layout/rotated-box-element.d.ts +35 -0
- package/dist/elements/layout/rotated-box-element.js +54 -0
- package/dist/elements/layout/rotated-element.d.ts +32 -0
- package/dist/elements/layout/rotated-element.js +41 -0
- package/dist/elements/layout/sized-container-element.d.ts +2 -2
- package/dist/elements/layout/sized-container-element.js +6 -11
- package/dist/elements/layout/struct-group.d.ts +28 -0
- package/dist/elements/layout/struct-group.js +42 -0
- package/dist/elements/line-element.d.ts +3 -3
- package/dist/elements/line-element.js +5 -10
- package/dist/elements/page-element.d.ts +26 -6
- package/dist/elements/page-element.js +72 -27
- package/dist/elements/pdf-document-element.d.ts +10 -4
- package/dist/elements/pdf-document-element.js +11 -10
- package/dist/elements/pdf-element.d.ts +150 -4
- package/dist/elements/pdf-element.js +138 -20
- package/dist/elements/rectangle-element.d.ts +52 -11
- package/dist/elements/rectangle-element.js +129 -36
- package/dist/elements/row-element.d.ts +45 -4
- package/dist/elements/row-element.js +66 -18
- package/dist/elements/text-element.d.ts +132 -11
- package/dist/elements/text-element.js +206 -56
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -19
- package/dist/ir/display-list.d.ts +197 -8
- package/dist/ir/display-list.js +8 -2
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +61 -0
- package/dist/layout/box-constraints.js +88 -6
- package/dist/layout/collect-images.d.ts +9 -0
- package/dist/layout/collect-images.js +34 -0
- package/dist/layout/fragmentation.d.ts +20 -1
- package/dist/layout/fragmentation.js +74 -18
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/platform/browser-fs.d.ts +2 -0
- package/dist/platform/browser-fs.js +9 -0
- package/dist/platform/browser-image.d.ts +5 -0
- package/dist/platform/browser-image.js +13 -0
- package/dist/platform/node-fs.d.ts +2 -0
- package/dist/platform/node-fs.js +10 -0
- package/dist/platform/node-image.d.ts +5 -0
- package/dist/platform/node-image.js +9 -0
- package/dist/renderer/anchor-renderer.d.ts +6 -0
- package/dist/renderer/anchor-renderer.js +11 -0
- package/dist/renderer/bookmark-renderer.d.ts +6 -0
- package/dist/renderer/bookmark-renderer.js +11 -0
- package/dist/renderer/container-renderer.d.ts +3 -3
- package/dist/renderer/container-renderer.js +11 -28
- package/dist/renderer/default-text-style-renderer.d.ts +6 -0
- package/dist/renderer/default-text-style-renderer.js +10 -0
- package/dist/renderer/deferred-renderer.d.ts +3 -3
- package/dist/renderer/deferred-renderer.js +8 -23
- package/dist/renderer/emoji-image.d.ts +9 -0
- package/dist/renderer/emoji-image.js +68 -0
- package/dist/renderer/expanded-renderer.d.ts +3 -3
- package/dist/renderer/expanded-renderer.js +6 -21
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.d.ts +4 -4
- package/dist/renderer/image-renderer.js +86 -75
- package/dist/renderer/index.d.ts +10 -10
- package/dist/renderer/index.js +10 -26
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/line-renderer.d.ts +3 -3
- package/dist/renderer/line-renderer.js +13 -28
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/padding-renderer.d.ts +3 -3
- package/dist/renderer/padding-renderer.js +6 -21
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-builder-renderer.d.ts +6 -0
- package/dist/renderer/page-builder-renderer.js +11 -0
- package/dist/renderer/page-renderer.d.ts +2 -2
- package/dist/renderer/page-renderer.js +115 -77
- package/dist/renderer/pdf-backend.d.ts +31 -4
- package/dist/renderer/pdf-backend.js +287 -49
- package/dist/renderer/pdf-config.js +4 -7
- package/dist/renderer/pdf-document-class.d.ts +5 -5
- package/dist/renderer/pdf-document-class.js +27 -41
- package/dist/renderer/pdf-document-renderer.d.ts +19 -10
- package/dist/renderer/pdf-document-renderer.js +135 -91
- package/dist/renderer/pdf-renderer.d.ts +2 -2
- package/dist/renderer/pdf-renderer.js +165 -90
- package/dist/renderer/positioned-renderer.d.ts +6 -0
- package/dist/renderer/positioned-renderer.js +10 -0
- package/dist/renderer/rectangle-renderer.d.ts +3 -3
- package/dist/renderer/rectangle-renderer.js +49 -44
- package/dist/renderer/repeating-header-renderer.d.ts +3 -3
- package/dist/renderer/repeating-header-renderer.js +10 -26
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/row-renderer.d.ts +3 -3
- package/dist/renderer/row-renderer.js +11 -28
- package/dist/renderer/struct-group-renderer.d.ts +10 -0
- package/dist/renderer/struct-group-renderer.js +24 -0
- package/dist/renderer/text-renderer.d.ts +9 -5
- package/dist/renderer/text-renderer.js +437 -89
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +45 -0
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/emoji-codepoints.d.ts +1 -0
- package/dist/text/emoji-codepoints.js +29 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/line-breaker.d.ts +50 -12
- package/dist/text/line-breaker.js +158 -37
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +60 -0
- package/dist/text/text-style.js +79 -0
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +76 -17
- package/dist/utils/bytes.d.ts +24 -0
- package/dist/utils/bytes.js +76 -0
- package/dist/utils/dest-registry.d.ts +22 -0
- package/dist/utils/dest-registry.js +32 -0
- package/dist/utils/flex-layout.d.ts +36 -9
- package/dist/utils/flex-layout.js +207 -30
- package/dist/utils/font-metrics.d.ts +20 -1
- package/dist/utils/font-metrics.js +1 -2
- package/dist/utils/font-path.js +3 -6
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/image-helper.d.ts +6 -5
- package/dist/utils/image-helper.js +101 -111
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/md5.d.ts +4 -0
- package/dist/utils/md5.js +79 -0
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +79 -0
- package/dist/utils/pdf-object-manager.d.ts +110 -8
- package/dist/utils/pdf-object-manager.js +583 -141
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +23 -6
- package/dist/utils/struct-tree.d.ts +44 -0
- package/dist/utils/struct-tree.js +129 -0
- package/dist/utils/ttf-parser.d.ts +134 -2
- package/dist/utils/ttf-parser.js +873 -50
- package/dist/utils/ttf-subsetter.d.ts +1 -1
- package/dist/utils/ttf-subsetter.js +40 -42
- package/dist/utils/ua-xmp.d.ts +9 -0
- package/dist/utils/ua-xmp.js +32 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.d.ts +2 -2
- package/dist/validators/element-validator.js +31 -26
- package/package.json +22 -2
|
@@ -1,57 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PDFObjectManager = exports.FontStyle = void 0;
|
|
37
|
-
const page_sizes_1 = require("../constants/page-sizes");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const crypto_1 = require("crypto");
|
|
41
|
-
const zlib_1 = require("zlib");
|
|
42
|
-
const afm_parser_1 = require("./afm-parser");
|
|
43
|
-
const ttf_parser_1 = require("./ttf-parser");
|
|
44
|
-
const ttf_subsetter_1 = require("./ttf-subsetter");
|
|
1
|
+
import { pageFormats, PageSize } from "../constants/page-sizes.js";
|
|
2
|
+
import { STANDARD_AFM } from "../assets/font-data.js";
|
|
3
|
+
import { md5, md5Hex } from "./md5.js";
|
|
4
|
+
import { zlibSync } from "fflate";
|
|
5
|
+
import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
|
|
6
|
+
import { AFMParser } from "./afm-parser.js";
|
|
7
|
+
import { mergeSpans, TTFParser } from "./ttf-parser.js";
|
|
8
|
+
import { isWoff, woffToSfnt } from "./woff.js";
|
|
9
|
+
import { subsetTTF } from "./ttf-subsetter.js";
|
|
10
|
+
import { shapeRun } from "../text/shape.js";
|
|
11
|
+
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
12
|
+
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
13
|
+
import { StructTree } from "./struct-tree.js";
|
|
14
|
+
import { OutlineBuilder } from "./outline.js";
|
|
15
|
+
import { DestRegistry } from "./dest-registry.js";
|
|
16
|
+
import { AcroFormCollector } from "../forms/acroform.js";
|
|
45
17
|
// Enums come from the leaf config module (never in a cycle); the config type is
|
|
46
18
|
// erased at runtime so it can come from the cyclic module safely.
|
|
47
|
-
|
|
48
|
-
var FontStyle;
|
|
19
|
+
import { ColorMode, Orientation } from "../renderer/pdf-config.js";
|
|
20
|
+
export var FontStyle;
|
|
49
21
|
(function (FontStyle) {
|
|
50
22
|
FontStyle["Normal"] = "normal";
|
|
51
23
|
FontStyle["Bold"] = "bold";
|
|
52
24
|
FontStyle["Italic"] = "italic";
|
|
53
25
|
FontStyle["BoldItalic"] = "boldItalic";
|
|
54
|
-
})(FontStyle || (
|
|
26
|
+
})(FontStyle || (FontStyle = {}));
|
|
27
|
+
// Escapes a string into a valid PDF /Name token. A raw space or delimiter in a /Name (e.g. a font
|
|
28
|
+
// family like "Great Vibes" or "Times New Roman") breaks the dictionary, so such chars become #XX.
|
|
29
|
+
const pdfName = (s) => s.replace(/[^\x21-\x7e]|[#()<>[\]{}/%]/g, (c) => "#" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
|
|
55
30
|
// Suffix appended to an embedded font's PDF /BaseFont so each style variant of a family keeps
|
|
56
31
|
// a distinct name (the variants are otherwise selected by the font resource, not the name).
|
|
57
32
|
const STYLE_SUFFIX = {
|
|
@@ -152,7 +127,19 @@ class FontManager {
|
|
|
152
127
|
return `${fontName}-${fontStyle}`;
|
|
153
128
|
}
|
|
154
129
|
}
|
|
155
|
-
class PDFObjectManager {
|
|
130
|
+
export class PDFObjectManager {
|
|
131
|
+
get struct() {
|
|
132
|
+
return this._struct;
|
|
133
|
+
}
|
|
134
|
+
get outline() {
|
|
135
|
+
return this._outline;
|
|
136
|
+
}
|
|
137
|
+
get dests() {
|
|
138
|
+
return this._dests;
|
|
139
|
+
}
|
|
140
|
+
get acroform() {
|
|
141
|
+
return this._acroform;
|
|
142
|
+
}
|
|
156
143
|
constructor(pageSize) {
|
|
157
144
|
this.objects = [];
|
|
158
145
|
this.objectPositions = [];
|
|
@@ -160,24 +147,55 @@ class PDFObjectManager {
|
|
|
160
147
|
this.fonts = new FontManager(); // Stores the fonts
|
|
161
148
|
this.images = new ImageManager(); // Stores the images (object numbers and names)
|
|
162
149
|
this.extGStates = new ExtGStateManager(); // Transparency states
|
|
163
|
-
this.
|
|
150
|
+
this.shadings = new Map(); // gradient resource name -> object number
|
|
151
|
+
this.pageFormat = pageFormats[PageSize.A4];
|
|
164
152
|
this.afmParsers = [];
|
|
165
153
|
// Embedded TrueType fonts, keyed by the name the user registers them under. When a font
|
|
166
154
|
// name is in here the metric/emission paths take the TTF branch instead of the AFM one.
|
|
155
|
+
// Nested by family, then by style. The flat `${name}-${style}` key it replaced had to be BUILT on
|
|
156
|
+
// every lookup, and the lookups happen once per character - the string allocation alone was a third
|
|
157
|
+
// of a custom-font render. `customFontEmit` below keeps the flat key: it is touched once per face.
|
|
167
158
|
this.customFonts = new Map();
|
|
159
|
+
// Memoised getFontVerticals answers, family -> style -> metrics. Cleared when a font registers.
|
|
160
|
+
this.verticalsCache = new Map();
|
|
161
|
+
// Same, for getFontDecoration.
|
|
162
|
+
this.decorationCache = new Map();
|
|
168
163
|
// Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
|
|
169
164
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
170
165
|
this.customFontEmit = new Map();
|
|
166
|
+
// Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
|
|
167
|
+
this.shapeCache = new Map();
|
|
168
|
+
// Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
|
|
169
|
+
// ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
|
|
170
|
+
this.shapedOrigins = new Map();
|
|
171
171
|
// Embedded file attachments (their /Filespec object numbers + display names). Referenced from
|
|
172
172
|
// the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
|
|
173
173
|
this.attachments = [];
|
|
174
174
|
// PDF header version (PDF/A-3 needs 1.7) and whether to write a trailer /ID (PDF/A needs one).
|
|
175
|
-
// Both default to the pre-PDF/A
|
|
175
|
+
// Both default to the pre-PDF/A behavior, so a normal document stays byte-identical.
|
|
176
176
|
this.pdfVersion = "1.4";
|
|
177
177
|
this.documentId = false;
|
|
178
178
|
this.compress = false;
|
|
179
|
+
this.overflowPolicy = "error";
|
|
180
|
+
this.encJobs = [];
|
|
181
|
+
this.strJobs = [];
|
|
182
|
+
// Accessible (PDF/UA) tagging, off by default (byte-identical output); the API turns it on.
|
|
183
|
+
this._struct = new StructTree();
|
|
184
|
+
// Document outline (bookmarks). Empty unless a `Bookmark` was placed; then finalize() emits /Outlines.
|
|
185
|
+
this._outline = new OutlineBuilder();
|
|
186
|
+
// Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
|
|
187
|
+
// a /Names /Dests tree.
|
|
188
|
+
this._dests = new DestRegistry();
|
|
189
|
+
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
190
|
+
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
191
|
+
this._acroform = new AcroFormCollector();
|
|
192
|
+
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
193
|
+
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
194
|
+
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
195
|
+
// fonts finer than a user's own font, which is worse than none. Flip to on when GPOS lands.
|
|
196
|
+
this.kerningEnabled = false;
|
|
179
197
|
if (pageSize)
|
|
180
|
-
this.pageFormat =
|
|
198
|
+
this.pageFormat = pageFormats[pageSize];
|
|
181
199
|
this.fillConfigWithStandardValues();
|
|
182
200
|
}
|
|
183
201
|
// Adds an object and returns its number
|
|
@@ -198,40 +216,102 @@ class PDFObjectManager {
|
|
|
198
216
|
setCompress(on) {
|
|
199
217
|
this.compress = on;
|
|
200
218
|
}
|
|
219
|
+
// Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
|
|
220
|
+
// seeds it into the layout-context root so packChildren can act on it.
|
|
221
|
+
setOverflowPolicy(policy) {
|
|
222
|
+
this.overflowPolicy = policy;
|
|
223
|
+
}
|
|
224
|
+
getOverflowPolicy() {
|
|
225
|
+
return this.overflowPolicy;
|
|
226
|
+
}
|
|
227
|
+
// A unique, binary-safe placeholder for a stream's bytes, swapped for ciphertext in finalizeEncryption().
|
|
228
|
+
encToken(id) {
|
|
229
|
+
return `JENC:${id}`;
|
|
230
|
+
}
|
|
231
|
+
// The same trick for a literal STRING. A separate marker because the two are swapped differently: a
|
|
232
|
+
// stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
|
|
233
|
+
strToken(id) {
|
|
234
|
+
return `\x00\x02JSTR:${id}\x02\x00`;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
|
|
238
|
+
* bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
|
|
239
|
+
*
|
|
240
|
+
* Without a security handler this is the escaped literal we have always written, so output stays
|
|
241
|
+
* byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
|
|
242
|
+
* their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
|
|
243
|
+
* here were exactly the values a password is supposed to protect (ISSUE-7).
|
|
244
|
+
*
|
|
245
|
+
* A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
|
|
246
|
+
*/
|
|
247
|
+
pdfString(text) {
|
|
248
|
+
if (!this.security) {
|
|
249
|
+
return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
|
|
250
|
+
}
|
|
251
|
+
// Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
|
|
252
|
+
// No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
|
|
253
|
+
const bytes = new Uint8Array(getArrayBuffer(text));
|
|
254
|
+
const id = this.strJobs.push(bytes) - 1;
|
|
255
|
+
return this.strToken(id);
|
|
256
|
+
}
|
|
257
|
+
// The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
|
|
258
|
+
// a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
|
|
259
|
+
// finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
|
|
260
|
+
streamPayload(bytes) {
|
|
261
|
+
if (!this.security)
|
|
262
|
+
return { body: latin1FromBytes(bytes), length: bytes.length };
|
|
263
|
+
const id = this.encJobs.push(bytes) - 1;
|
|
264
|
+
return { body: this.encToken(id), length: 16 + (Math.floor(bytes.length / 16) + 1) * 16 };
|
|
265
|
+
}
|
|
201
266
|
// Builds a stream object body: `<< extraDict /Length n >> stream … endstream`, FlateDecode-compressed
|
|
202
|
-
// when enabled AND it actually helps (tiny streams aren't inflated).
|
|
203
|
-
//
|
|
267
|
+
// when enabled AND it actually helps (tiny streams aren't inflated). The payload rides through
|
|
268
|
+
// streamPayload, so it is encrypted uniformly when a security handler is set.
|
|
204
269
|
stream(extraDict, data) {
|
|
205
270
|
const head = extraDict ? extraDict + " " : "";
|
|
271
|
+
let body = data;
|
|
272
|
+
let filter = "";
|
|
206
273
|
if (this.compress) {
|
|
207
|
-
const z = (
|
|
274
|
+
const z = zlibSync(data);
|
|
208
275
|
if (z.length < data.length) {
|
|
209
|
-
|
|
276
|
+
body = z;
|
|
277
|
+
filter = "/Filter /FlateDecode ";
|
|
210
278
|
}
|
|
211
279
|
}
|
|
212
|
-
|
|
280
|
+
const p = this.streamPayload(body);
|
|
281
|
+
return `<< ${head}${filter}/Length ${p.length} >>\nstream\n${p.body}\nendstream`;
|
|
213
282
|
}
|
|
214
283
|
// Adds a page content stream (compressed when enabled). The caller passes the raw operator string.
|
|
284
|
+
// Bytes go through the Windows-1252 encoder (NOT a latin1 low-byte cast): a Tj literal may carry a
|
|
285
|
+
// CP1252 char like "…"/"—"/"€" whose codepoint is > 0xFF, and latin1 would mangle it (… -> "&").
|
|
286
|
+
// For every char <= 0xFF the encoder passes the byte through, so existing streams stay byte-identical.
|
|
215
287
|
addContentStream(content) {
|
|
216
|
-
return this.addObject(this.stream("",
|
|
288
|
+
return this.addObject(this.stream("", new Uint8Array(getArrayBuffer(content))));
|
|
289
|
+
}
|
|
290
|
+
// A Form XObject: a self-contained content stream with its own /BBox (+ optional /Resources). Used for
|
|
291
|
+
// form-field appearance streams (/AP). Routes through the same choke-point as page content, so
|
|
292
|
+
// compression and encryption apply. Returns the object number.
|
|
293
|
+
addFormXObject(bbox, content, resources) {
|
|
294
|
+
const dict = `/Type /XObject /Subtype /Form /BBox ${bbox}` +
|
|
295
|
+
(resources ? ` /Resources << ${resources} >>` : "");
|
|
296
|
+
return this.addObject(this.stream(dict, new Uint8Array(getArrayBuffer(content))));
|
|
217
297
|
}
|
|
218
298
|
changePDFConfig(config) {
|
|
219
|
-
this.pdfConfig =
|
|
299
|
+
this.pdfConfig = { ...this.pdfConfig, ...config };
|
|
220
300
|
}
|
|
221
301
|
getPDFConfig() {
|
|
222
302
|
return this.pdfConfig;
|
|
223
303
|
}
|
|
224
304
|
fillConfigWithStandardValues() {
|
|
225
305
|
this.pdfConfig = {
|
|
226
|
-
orientation:
|
|
306
|
+
orientation: Orientation.portrait,
|
|
227
307
|
defaultFont: {
|
|
228
308
|
fontFamily: "Helvetica",
|
|
229
309
|
fontSize: 12,
|
|
230
310
|
fontStyle: FontStyle.Normal,
|
|
231
311
|
},
|
|
232
|
-
pageSize:
|
|
312
|
+
pageSize: PageSize.A4,
|
|
233
313
|
margin: { left: 72, top: 72, bottom: 72, right: 72 },
|
|
234
|
-
colorMode:
|
|
314
|
+
colorMode: ColorMode.color,
|
|
235
315
|
};
|
|
236
316
|
}
|
|
237
317
|
// Calculates the total length of the document in bytes (for XRef)
|
|
@@ -240,10 +320,8 @@ class PDFObjectManager {
|
|
|
240
320
|
for (let i = 0; i < this.objects.length; i++) {
|
|
241
321
|
const obj = this.objects[i];
|
|
242
322
|
const objectContent = `${i + 1} 0 obj\n${obj}\nendobj\n`;
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
// Add the actual byte length to the total length
|
|
246
|
-
length += encodedContent.length;
|
|
323
|
+
// The body is a latin1/binary string (every char <= 0xFF), so its length IS its byte length.
|
|
324
|
+
length += objectContent.length;
|
|
247
325
|
}
|
|
248
326
|
return length;
|
|
249
327
|
}
|
|
@@ -256,7 +334,25 @@ class PDFObjectManager {
|
|
|
256
334
|
return this.parentObjectNumber;
|
|
257
335
|
}
|
|
258
336
|
// Registers an image
|
|
259
|
-
registerImage(width, height, imageType, imageData) {
|
|
337
|
+
registerImage(width, height, imageType, imageData, smaskData) {
|
|
338
|
+
// A transparent PNG carries its alpha as a separate DeviceGray /SMask image the viewer composites with.
|
|
339
|
+
let smaskEntry = "";
|
|
340
|
+
if (smaskData) {
|
|
341
|
+
const s = this.streamPayload(bytesFromLatin1(smaskData));
|
|
342
|
+
const smaskObject = `<< /Type /XObject
|
|
343
|
+
/Subtype /Image
|
|
344
|
+
/Width ${width}
|
|
345
|
+
/Height ${height}
|
|
346
|
+
/ColorSpace /DeviceGray
|
|
347
|
+
/BitsPerComponent 8
|
|
348
|
+
/Filter /FlateDecode
|
|
349
|
+
/Length ${s.length} >>
|
|
350
|
+
stream
|
|
351
|
+
${s.body}
|
|
352
|
+
endstream`;
|
|
353
|
+
smaskEntry = ` /SMask ${this.addObject(smaskObject)} 0 R\n`;
|
|
354
|
+
}
|
|
355
|
+
const img = this.streamPayload(bytesFromLatin1(imageData));
|
|
260
356
|
const imageObject = `<< /Type /XObject
|
|
261
357
|
/Subtype /Image
|
|
262
358
|
/Width ${width}
|
|
@@ -264,9 +360,9 @@ class PDFObjectManager {
|
|
|
264
360
|
/ColorSpace /DeviceRGB
|
|
265
361
|
/BitsPerComponent 8
|
|
266
362
|
/Filter /${imageType}
|
|
267
|
-
/Length ${
|
|
363
|
+
${smaskEntry} /Length ${img.length} >>
|
|
268
364
|
stream
|
|
269
|
-
${
|
|
365
|
+
${img.body}
|
|
270
366
|
endstream`;
|
|
271
367
|
// Add the image and its object number to the image manager - return the object number
|
|
272
368
|
const imageObjectNumber = this.addObject(imageObject);
|
|
@@ -278,13 +374,11 @@ endstream`;
|
|
|
278
374
|
// `relationship` is the /AFRelationship (e.g. "Data" for the ZUGFeRD factur-x.xml). Binary bytes
|
|
279
375
|
// ride as a latin1 string, like images/fonts (the final encoder passes 0x00-0xFF through).
|
|
280
376
|
attachFile(name, data, opts = {}) {
|
|
281
|
-
|
|
282
|
-
const subtype = ((_a = opts.mimeType) !== null && _a !== void 0 ? _a : "application/octet-stream").replace(/\//g, "#2F");
|
|
377
|
+
const subtype = (opts.mimeType ?? "application/octet-stream").replace(/\//g, "#2F");
|
|
283
378
|
const embedded = this.addObject(this.stream(`/Type /EmbeddedFile /Subtype /${subtype} /Params << /Size ${data.length} >>`, data));
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
`/AFRelationship /${(_b = opts.relationship) !== null && _b !== void 0 ? _b : "Unspecified"}${desc} ` +
|
|
379
|
+
const desc = opts.description ? ` /Desc ${this.pdfString(opts.description)}` : "";
|
|
380
|
+
const filespec = this.addObject(`<< /Type /Filespec /F ${this.pdfString(name)} /UF ${this.pdfString(name)} ` +
|
|
381
|
+
`/AFRelationship /${opts.relationship ?? "Unspecified"}${desc} ` +
|
|
288
382
|
`/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
|
|
289
383
|
this.attachments.push({ name, filespec });
|
|
290
384
|
}
|
|
@@ -302,11 +396,10 @@ endstream`;
|
|
|
302
396
|
// Embeds an ICC profile and a PDF/A /OutputIntent that points at it (catalog /OutputIntents).
|
|
303
397
|
// `icc` are the raw profile bytes (an RGB profile, /N 3 - e.g. sRGB).
|
|
304
398
|
setOutputIntent(icc, opts = {}) {
|
|
305
|
-
var _a, _b;
|
|
306
399
|
const profile = this.addObject(this.stream("/N 3", icc));
|
|
307
400
|
this.outputIntent = this.addObject(`<< /Type /OutputIntent /S /GTS_PDFA1 ` +
|
|
308
|
-
`/OutputConditionIdentifier
|
|
309
|
-
`/Info
|
|
401
|
+
`/OutputConditionIdentifier ${this.pdfString(opts.identifier ?? "sRGB")} ` +
|
|
402
|
+
`/Info ${this.pdfString(opts.info ?? "sRGB IEC61966-2.1")} /DestOutputProfile ${profile} 0 R >>`);
|
|
310
403
|
}
|
|
311
404
|
getOutputIntent() {
|
|
312
405
|
return this.outputIntent;
|
|
@@ -331,6 +424,43 @@ endstream`;
|
|
|
331
424
|
enableDocumentId() {
|
|
332
425
|
this.documentId = true;
|
|
333
426
|
}
|
|
427
|
+
// Turn on encryption: the handler encrypts every stream and supplies the /Encrypt dict. Forces a trailer
|
|
428
|
+
// /ID (encrypted PDFs require one) and PDF 2.0 (AES-256 R6). PDF/A forbids encryption - guarded at finalize.
|
|
429
|
+
setSecurityHandler(handler) {
|
|
430
|
+
this.security = handler;
|
|
431
|
+
this.documentId = true;
|
|
432
|
+
this.setPdfVersion("2.0");
|
|
433
|
+
}
|
|
434
|
+
// One async pass at the very end: refuse to encrypt a PDF/A document, then encrypt every registered stream
|
|
435
|
+
// (swap each placeholder for its ciphertext) and add the /Encrypt object the trailer references.
|
|
436
|
+
async finalizeEncryption() {
|
|
437
|
+
if (!this.security)
|
|
438
|
+
return;
|
|
439
|
+
if (this.outputIntent !== undefined || this.attachments.length > 0) {
|
|
440
|
+
throw new Error("@jasy/pdf: cannot encrypt a PDF/A document - PDF/A forbids encryption.");
|
|
441
|
+
}
|
|
442
|
+
for (let id = 0; id < this.encJobs.length; id++) {
|
|
443
|
+
const ciphertext = latin1FromBytes(await this.security.encrypt(this.encJobs[id]));
|
|
444
|
+
const token = this.encToken(id);
|
|
445
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
446
|
+
// Function replacement: a string replacement would interpret `$&`/`$\``/... in the random ciphertext.
|
|
447
|
+
if (idx >= 0)
|
|
448
|
+
this.objects[idx] = this.objects[idx].replace(token, () => ciphertext);
|
|
449
|
+
}
|
|
450
|
+
// Strings: the same idea, but a string is replaced WHOLE - `(text)` becomes `<ciphertext>`. Hex,
|
|
451
|
+
// because the ciphertext is binary and a hex string has no escape rules to get wrong.
|
|
452
|
+
for (let id = 0; id < this.strJobs.length; id++) {
|
|
453
|
+
const cipher = await this.security.encrypt(this.strJobs[id]);
|
|
454
|
+
const hex = Array.from(cipher, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
|
455
|
+
const token = this.strToken(id);
|
|
456
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
457
|
+
if (idx >= 0)
|
|
458
|
+
this.objects[idx] = this.objects[idx].replace(token, () => `<${hex}>`);
|
|
459
|
+
}
|
|
460
|
+
// Added LAST on purpose: the /Encrypt dictionary's own strings must stay in the clear (7.6.2), and
|
|
461
|
+
// they would otherwise be swept up by the pass above.
|
|
462
|
+
this.encryptObjNum = this.addObject(`<< ${this.security.encryptDict()} >>`);
|
|
463
|
+
}
|
|
334
464
|
// Registers (or reuses) a transparency graphics state and returns its resource name
|
|
335
465
|
// (e.g. "GS1"). `fillAlpha` -> /ca, `strokeAlpha` -> /CA. Deduped by the alpha pair.
|
|
336
466
|
registerExtGState(fillAlpha, strokeAlpha) {
|
|
@@ -349,25 +479,73 @@ endstream`;
|
|
|
349
479
|
getAllExtGStatesRaw() {
|
|
350
480
|
return this.extGStates.getAll();
|
|
351
481
|
}
|
|
482
|
+
// Registers a gradient as a PDF shading (+ its color-stop function) and returns the resource name
|
|
483
|
+
// (e.g. "Sh1"). Used to fill a color-glyph layer with a COLR v1 gradient. Not deduped: each layer
|
|
484
|
+
// carries page-absolute coordinates, so two calls rarely match.
|
|
485
|
+
registerShading(g) {
|
|
486
|
+
const fn = this.buildShadingFunction(g.stops);
|
|
487
|
+
const n = (x) => x.toFixed(3);
|
|
488
|
+
const coords = g.type === "linear"
|
|
489
|
+
? `${n(g.x0)} ${n(g.y0)} ${n(g.x1)} ${n(g.y1)}`
|
|
490
|
+
: `${n(g.x0)} ${n(g.y0)} ${n(g.r0)} ${n(g.x1)} ${n(g.y1)} ${n(g.r1)}`;
|
|
491
|
+
const shadingType = g.type === "linear" ? 2 : 3;
|
|
492
|
+
// PDF shadings extend by clamping the end colors; "repeat"/"reflect" would need a tiling pattern,
|
|
493
|
+
// so we approximate them as clamp (pad) for now - correct for the common pad case.
|
|
494
|
+
const objectNumber = this.addObject(`<< /ShadingType ${shadingType} /ColorSpace /DeviceRGB /Coords [${coords}] ` +
|
|
495
|
+
`/Function ${fn} 0 R /Extend [true true] >>`);
|
|
496
|
+
const name = `Sh${this.shadings.size + 1}`;
|
|
497
|
+
this.shadings.set(name, objectNumber);
|
|
498
|
+
return name;
|
|
499
|
+
}
|
|
500
|
+
// Builds the PDF function mapping t in [0,1] to a color along the stops. Two stops collapse to one
|
|
501
|
+
// exponential (linear) function; more stops stitch one linear piece per interval (FunctionType 3).
|
|
502
|
+
buildShadingFunction(stops) {
|
|
503
|
+
const c = (i) => stops[i].color.toPDFColorString(); // "r g b" in 0..1
|
|
504
|
+
// Malformed / empty color line: fall back to opaque black instead of indexing an empty array.
|
|
505
|
+
if (stops.length === 0) {
|
|
506
|
+
return this.addObject("<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [0 0 0] /N 1 >>");
|
|
507
|
+
}
|
|
508
|
+
if (stops.length <= 2) {
|
|
509
|
+
const c0 = c(0);
|
|
510
|
+
const c1 = c(stops.length - 1);
|
|
511
|
+
return this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c0}] /C1 [${c1}] /N 1 >>`);
|
|
512
|
+
}
|
|
513
|
+
const pieces = [];
|
|
514
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
515
|
+
pieces.push(this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c(i)}] /C1 [${c(i + 1)}] /N 1 >>`));
|
|
516
|
+
}
|
|
517
|
+
const functions = pieces.map((p) => `${p} 0 R`).join(" ");
|
|
518
|
+
const bounds = stops
|
|
519
|
+
.slice(1, -1)
|
|
520
|
+
.map((s) => s.offset.toFixed(4))
|
|
521
|
+
.join(" ");
|
|
522
|
+
const encode = pieces.map(() => "0 1").join(" ");
|
|
523
|
+
return this.addObject(`<< /FunctionType 3 /Domain [0 1] /Functions [${functions}] /Bounds [${bounds}] /Encode [${encode}] >>`);
|
|
524
|
+
}
|
|
525
|
+
// Returns all registered gradient shadings (name -> object number) for the page /Resources.
|
|
526
|
+
getAllShadingsRaw() {
|
|
527
|
+
return this.shadings;
|
|
528
|
+
}
|
|
352
529
|
// Registers a font
|
|
353
530
|
registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
|
|
354
531
|
if (this.fonts.hasFont(fontName, fontStyle)) {
|
|
355
532
|
return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
|
|
356
533
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
534
|
+
this.verticalsCache.delete(fontName);
|
|
535
|
+
this.decorationCache.delete(fontName);
|
|
536
|
+
const data = STANDARD_AFM[fullName];
|
|
537
|
+
if (data !== undefined) {
|
|
360
538
|
this.afmParsers.push({
|
|
361
539
|
fontName,
|
|
362
540
|
fontStyle,
|
|
363
541
|
fullFontName: fullName,
|
|
364
|
-
parser: new
|
|
542
|
+
parser: new AFMParser(data),
|
|
365
543
|
});
|
|
366
544
|
}
|
|
367
545
|
const resourceNumber = this.objects.length + 1; // New resource number
|
|
368
546
|
const fontNumber = this.fonts.getLastFontIndex() + 1; // New font index number
|
|
369
547
|
this.fonts.addFont(fontName, fontNumber, resourceNumber, fontStyle); // Store it
|
|
370
|
-
const fontObject = `<</BaseFont/${fullName}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
|
|
548
|
+
const fontObject = `<</BaseFont/${pdfName(fullName)}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
|
|
371
549
|
this.addObject(fontObject);
|
|
372
550
|
return {
|
|
373
551
|
fontIndex: fontNumber,
|
|
@@ -380,10 +558,19 @@ endstream`;
|
|
|
380
558
|
// metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
|
|
381
559
|
// separate .ttf files registered under the same name with a different style.
|
|
382
560
|
registerCustomFont(name, data, style = FontStyle.Normal) {
|
|
383
|
-
|
|
384
|
-
if (
|
|
561
|
+
let byStyle = this.customFonts.get(name);
|
|
562
|
+
if (!byStyle) {
|
|
563
|
+
byStyle = new Map();
|
|
564
|
+
this.customFonts.set(name, byStyle);
|
|
565
|
+
}
|
|
566
|
+
if (byStyle.has(style))
|
|
385
567
|
return;
|
|
386
|
-
|
|
568
|
+
// A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
|
|
569
|
+
// font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
|
|
570
|
+
// second container format.
|
|
571
|
+
byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
|
|
572
|
+
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
573
|
+
this.decorationCache.delete(name);
|
|
387
574
|
// Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
|
|
388
575
|
// so a registered-but-never-rendered face - e.g. a bundled family the document doesn't touch -
|
|
389
576
|
// costs nothing in the output. The metrics above are still available for layout immediately.
|
|
@@ -406,6 +593,7 @@ endstream`;
|
|
|
406
593
|
this.fonts.addFont(name, this.fonts.getLastFontIndex() + 1, type0, style, name, true);
|
|
407
594
|
this.customFontEmit.set(key, {
|
|
408
595
|
pdfName: name + STYLE_SUFFIX[style],
|
|
596
|
+
ttf: this.customFonts.get(name).get(style), // `style` is already resolved, so this exists
|
|
409
597
|
fontFile,
|
|
410
598
|
descriptor,
|
|
411
599
|
cidFont,
|
|
@@ -418,20 +606,18 @@ endstream`;
|
|
|
418
606
|
// tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
|
|
419
607
|
finalizeCustomFonts() {
|
|
420
608
|
for (const [key, e] of this.customFontEmit) {
|
|
421
|
-
const ttf =
|
|
609
|
+
const ttf = e.ttf;
|
|
422
610
|
const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
|
|
423
611
|
this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
|
|
424
612
|
this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
|
|
425
613
|
this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
|
|
426
|
-
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
|
|
614
|
+
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
|
|
427
615
|
this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
|
|
428
616
|
}
|
|
429
617
|
}
|
|
430
618
|
// A deterministic 6-uppercase-letter subset tag (PDF/A wants the "TAG+FontName" form).
|
|
431
619
|
subsetTag(pdfName, used) {
|
|
432
|
-
const h = (
|
|
433
|
-
.update(pdfName + [...used].sort((a, b) => a - b).join(","))
|
|
434
|
-
.digest();
|
|
620
|
+
const h = md5(new TextEncoder().encode(pdfName + [...used].sort((a, b) => a - b).join(",")));
|
|
435
621
|
let tag = "";
|
|
436
622
|
for (let i = 0; i < 6; i++)
|
|
437
623
|
tag += String.fromCharCode(65 + (h[i] % 26));
|
|
@@ -443,22 +629,74 @@ endstream`;
|
|
|
443
629
|
// The variant to actually use for (name, style): the requested style if registered, else the
|
|
444
630
|
// family's Normal as a clean fallback (e.g. bold chosen but no bold file), else undefined when
|
|
445
631
|
// `name` is not a custom family at all.
|
|
632
|
+
//
|
|
633
|
+
// This runs once per CHARACTER (getCharWidth -> getCustomFont), and every paragraph is measured
|
|
634
|
+
// several times (layout, pagination, drawing), so it is the hottest function in the whole renderer.
|
|
635
|
+
// The size check is what keeps a standard-14 document from paying for a feature it never uses: with
|
|
636
|
+
// no custom font registered, neither lookup could ever hit, so there is nothing to look up.
|
|
446
637
|
resolveCustomStyle(name, style = FontStyle.Normal) {
|
|
447
|
-
if (!name)
|
|
638
|
+
if (this.customFonts.size === 0 || !name)
|
|
639
|
+
return undefined;
|
|
640
|
+
const byStyle = this.customFonts.get(name);
|
|
641
|
+
if (!byStyle)
|
|
448
642
|
return undefined;
|
|
449
|
-
if (
|
|
643
|
+
if (byStyle.has(style))
|
|
450
644
|
return style;
|
|
451
|
-
|
|
452
|
-
return FontStyle.Normal;
|
|
453
|
-
return undefined;
|
|
645
|
+
return byStyle.has(FontStyle.Normal) ? FontStyle.Normal : undefined;
|
|
454
646
|
}
|
|
647
|
+
// The same lookup as above, but returning the font itself in ONE map walk instead of resolving the
|
|
648
|
+
// style and then looking the font up again under a freshly built key.
|
|
455
649
|
getCustomFont(name, style = FontStyle.Normal) {
|
|
456
|
-
|
|
457
|
-
|
|
650
|
+
if (this.customFonts.size === 0 || !name)
|
|
651
|
+
return undefined;
|
|
652
|
+
const byStyle = this.customFonts.get(name);
|
|
653
|
+
if (!byStyle)
|
|
654
|
+
return undefined;
|
|
655
|
+
return byStyle.get(style) ?? byStyle.get(FontStyle.Normal);
|
|
458
656
|
}
|
|
459
657
|
isCustomFont(name, style = FontStyle.Normal) {
|
|
460
658
|
return !!this.resolveCustomStyle(name, style);
|
|
461
659
|
}
|
|
660
|
+
// The parsed font for a color-capable custom family (COLR/CPAL), for drawing emoji as vector
|
|
661
|
+
// layers. Returns undefined for standard-14 fonts, unknown names, or fonts with no color glyphs -
|
|
662
|
+
// the caller then draws normal monochrome text.
|
|
663
|
+
getColorFont(name, style = FontStyle.Normal) {
|
|
664
|
+
const ttf = this.getCustomFont(name, style);
|
|
665
|
+
return ttf?.hasColorGlyphs() ? ttf : undefined;
|
|
666
|
+
}
|
|
667
|
+
// The document's color-emoji fallback font: a code point the current text font can't color-render
|
|
668
|
+
// is drawn from this font's color glyphs instead (so `Text("Hallo 😅")` works in one string). Set
|
|
669
|
+
// via `Document({ emoji })`; must name a registered color font.
|
|
670
|
+
setEmojiFont(name) {
|
|
671
|
+
this.emojiFontName = name;
|
|
672
|
+
}
|
|
673
|
+
getEmojiFont() {
|
|
674
|
+
return this.emojiFontName;
|
|
675
|
+
}
|
|
676
|
+
// Alternative color-emoji source: raster images (a CDN like Twemoji, react-pdf style). The renderer
|
|
677
|
+
// fetches `${url}${hexCodePoint}.${format}` per emoji and embeds it. The font source is preferred
|
|
678
|
+
// (native vector); this exists for parity + as an escape hatch.
|
|
679
|
+
setEmojiImageSource(url, format) {
|
|
680
|
+
this.emojiImage = { url, format };
|
|
681
|
+
}
|
|
682
|
+
getEmojiImageSource() {
|
|
683
|
+
return this.emojiImage;
|
|
684
|
+
}
|
|
685
|
+
// Whether `ttf` has a COLOR glyph for a code point (i.e. it would render it as color emoji).
|
|
686
|
+
/**
|
|
687
|
+
* Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
|
|
688
|
+
* standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
|
|
689
|
+
* Used to pick a family out of a fallback stack, per code point.
|
|
690
|
+
*/
|
|
691
|
+
hasGlyph(codePoint, fontFamily, fontStyle = FontStyle.Normal) {
|
|
692
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
693
|
+
if (ttf)
|
|
694
|
+
return ttf.getGlyphIndex(codePoint) !== 0;
|
|
695
|
+
return isWindows1252(codePoint);
|
|
696
|
+
}
|
|
697
|
+
colorRenders(ttf, codePoint) {
|
|
698
|
+
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
699
|
+
}
|
|
462
700
|
// The page font resource for the resolved variant - same resolution as getCustomFont, so the
|
|
463
701
|
// selected Type0 object and the emitted glyph ids always come from the SAME font file.
|
|
464
702
|
getCustomFontResource(name, style = FontStyle.Normal) {
|
|
@@ -468,6 +706,53 @@ endstream`;
|
|
|
468
706
|
this.ensureEmitted(name, resolved);
|
|
469
707
|
return this.fonts.getFont(name, resolved);
|
|
470
708
|
}
|
|
709
|
+
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
710
|
+
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
711
|
+
shapedGlyphCount(text, fontFamily, fontStyle) {
|
|
712
|
+
return this.shapeText(text, fontFamily, fontStyle)?.length;
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
716
|
+
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
717
|
+
* times (layout, pagination, drawing).
|
|
718
|
+
*/
|
|
719
|
+
shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
|
|
720
|
+
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
721
|
+
if (!resolved)
|
|
722
|
+
return undefined;
|
|
723
|
+
const key = `${this.customKey(fontFamily, resolved)}\u0000${text}`;
|
|
724
|
+
const cached = this.shapeCache.get(key);
|
|
725
|
+
if (cached !== undefined)
|
|
726
|
+
return cached ?? undefined;
|
|
727
|
+
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
728
|
+
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf) ?? null;
|
|
729
|
+
this.shapeCache.set(key, shaped);
|
|
730
|
+
if (shaped) {
|
|
731
|
+
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
732
|
+
const fontKey = this.customKey(fontFamily, resolved);
|
|
733
|
+
let origins = this.shapedOrigins.get(fontKey);
|
|
734
|
+
if (!origins)
|
|
735
|
+
this.shapedOrigins.set(fontKey, (origins = new Map()));
|
|
736
|
+
for (const g of shaped)
|
|
737
|
+
origins.set(g.glyph, g.codePoints);
|
|
738
|
+
}
|
|
739
|
+
return shaped ?? undefined;
|
|
740
|
+
}
|
|
741
|
+
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
742
|
+
* text. Recording each keeps it in the subset. */
|
|
743
|
+
registerGlyphs(name, glyphs, style = FontStyle.Normal) {
|
|
744
|
+
const resolved = this.resolveCustomStyle(name, style);
|
|
745
|
+
if (!resolved)
|
|
746
|
+
return "";
|
|
747
|
+
this.ensureEmitted(name, resolved);
|
|
748
|
+
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
749
|
+
let out = "";
|
|
750
|
+
for (const g of glyphs) {
|
|
751
|
+
emit.used.add(g);
|
|
752
|
+
out += g.toString(16).padStart(4, "0").toUpperCase();
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
471
756
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
472
757
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
473
758
|
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
@@ -475,27 +760,38 @@ endstream`;
|
|
|
475
760
|
if (!resolved)
|
|
476
761
|
return "";
|
|
477
762
|
this.ensureEmitted(name, resolved);
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
const used =
|
|
763
|
+
// Once per text run, not per character: the flat key is fine here.
|
|
764
|
+
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
765
|
+
const { ttf, used } = emit;
|
|
766
|
+
const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
|
|
767
|
+
// The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
|
|
768
|
+
const shaped = this.shapeText(text, name, style);
|
|
769
|
+
if (shaped) {
|
|
770
|
+
let out = "";
|
|
771
|
+
for (const g of shaped) {
|
|
772
|
+
used.add(g.glyph);
|
|
773
|
+
out += hex4(g.glyph);
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
481
777
|
let hex = "";
|
|
482
778
|
for (const ch of text) {
|
|
483
779
|
const gid = ttf.getGlyphIndex(ch.codePointAt(0));
|
|
484
780
|
used.add(gid); // record the glyph so the subset keeps it
|
|
485
|
-
hex += gid
|
|
781
|
+
hex += hex4(gid);
|
|
486
782
|
}
|
|
487
783
|
return hex;
|
|
488
784
|
}
|
|
489
785
|
// The SUBSET font program (only the used glyphs' outlines). Binary bytes survive as a latin1
|
|
490
786
|
// string - the final Windows-1252 encoder passes 0x00-0xFF through unchanged (see getArrayBuffer).
|
|
491
787
|
buildFontFile2(ttf, used) {
|
|
492
|
-
const bytes =
|
|
788
|
+
const bytes = subsetTTF(ttf.getData(), used);
|
|
493
789
|
// /Length1 is the UNCOMPRESSED font-program length (required even when FlateDecode'd).
|
|
494
790
|
return this.stream(`/Length1 ${bytes.length}`, bytes);
|
|
495
791
|
}
|
|
496
792
|
buildFontDescriptor(name, ttf, fontFile) {
|
|
497
793
|
const [x0, y0, x1, y1] = ttf.bbox;
|
|
498
|
-
return (`<< /Type /FontDescriptor /FontName /${name} /Flags 4 ` +
|
|
794
|
+
return (`<< /Type /FontDescriptor /FontName /${pdfName(name)} /Flags 4 ` +
|
|
499
795
|
`/FontBBox [${x0} ${y0} ${x1} ${y1}] /ItalicAngle 0 ` +
|
|
500
796
|
`/Ascent ${ttf.ascent} /Descent ${ttf.descent} /CapHeight ${ttf.ascent} /StemV 80 ` +
|
|
501
797
|
`/FontFile2 ${fontFile} 0 R >>`);
|
|
@@ -506,26 +802,33 @@ endstream`;
|
|
|
506
802
|
const all = ttf.glyphWidths();
|
|
507
803
|
const w = [...used]
|
|
508
804
|
.sort((a, b) => a - b)
|
|
509
|
-
.map((g) =>
|
|
805
|
+
.map((g) => `${g} [${all[g] ?? 1000}]`)
|
|
510
806
|
.join(" ");
|
|
511
|
-
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${name} ` +
|
|
512
|
-
`/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
|
|
807
|
+
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
|
|
808
|
+
`/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
|
|
513
809
|
`/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
|
|
514
810
|
`/DW 1000 /W [${w}] >>`);
|
|
515
811
|
}
|
|
516
812
|
// Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
|
|
517
813
|
// the used glyphs are listed, to match the subset.
|
|
518
|
-
buildToUnicode(ttf, used) {
|
|
814
|
+
buildToUnicode(ttf, used, shapedOrigins = new Map()) {
|
|
519
815
|
const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
|
|
816
|
+
// A code point above the BMP needs its surrogate PAIR here - a `bfchar` destination is UTF-16BE.
|
|
817
|
+
const utf16 = (code) => code > 0xffff
|
|
818
|
+
? hex4(0xd800 + ((code - 0x10000) >> 10)) + hex4(0xdc00 + ((code - 0x10000) & 0x3ff))
|
|
819
|
+
: hex4(code);
|
|
520
820
|
const rev = ttf.reverseCmap();
|
|
821
|
+
// A SHAPED glyph wins: it is often absent from the cmap (a ligature) or maps back to a
|
|
822
|
+
// presentation form rather than the letter that was written (a joining form), so the reverse
|
|
823
|
+
// cmap alone would make copied text wrong.
|
|
521
824
|
const entries = [...used]
|
|
522
825
|
.sort((a, b) => a - b)
|
|
523
|
-
.filter((g) => rev.has(g))
|
|
524
|
-
.map((g) => [g, rev.get(g)]);
|
|
826
|
+
.filter((g) => shapedOrigins.has(g) || rev.has(g))
|
|
827
|
+
.map((g) => [g, shapedOrigins.get(g) ?? [rev.get(g)]]);
|
|
525
828
|
const blocks = [];
|
|
526
829
|
for (let i = 0; i < entries.length; i += 100) {
|
|
527
830
|
const block = entries.slice(i, i + 100);
|
|
528
|
-
const lines = block.map(([gid,
|
|
831
|
+
const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
|
|
529
832
|
blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
|
|
530
833
|
}
|
|
531
834
|
const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
|
|
@@ -533,34 +836,159 @@ endstream`;
|
|
|
533
836
|
`/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n` +
|
|
534
837
|
`1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n${blocks.join("\n")}\n` +
|
|
535
838
|
`endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
|
|
536
|
-
return this.stream("",
|
|
839
|
+
return this.stream("", bytesFromLatin1(body));
|
|
537
840
|
}
|
|
538
841
|
buildType0(name, cidFont, toUnicode) {
|
|
539
|
-
return (`<< /Type /Font /Subtype /Type0 /BaseFont /${name} /Encoding /Identity-H ` +
|
|
842
|
+
return (`<< /Type /Font /Subtype /Type0 /BaseFont /${pdfName(name)} /Encoding /Identity-H ` +
|
|
540
843
|
`/DescendantFonts [${cidFont} 0 R] /ToUnicode ${toUnicode} 0 R >>`);
|
|
541
844
|
}
|
|
845
|
+
// The vertical metrics of a face, in em fractions - what seats the baseline and sizes the line
|
|
846
|
+
// box (see text/line-metrics.ts). An embedded font answers from its own `hhea`; a standard-14
|
|
847
|
+
// font from its AFM header. Nothing here is guessed: both numbers come out of the font data.
|
|
848
|
+
//
|
|
849
|
+
// Asked once per LINE (not per character), but a long document has a lot of lines and the
|
|
850
|
+
// standard-14 lookup below is a linear scan, so the answer is memoised per face. Registering a
|
|
851
|
+
// font clears the cache - a name can go from standard-14 to embedded.
|
|
852
|
+
getFontVerticals(fontFamily, fontStyle) {
|
|
853
|
+
let byStyle = this.verticalsCache.get(fontFamily);
|
|
854
|
+
const hit = byStyle?.get(fontStyle);
|
|
855
|
+
if (hit)
|
|
856
|
+
return hit;
|
|
857
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
858
|
+
const verticals = ttf
|
|
859
|
+
? {
|
|
860
|
+
ascent: ttf.ascent / 1000,
|
|
861
|
+
descent: -ttf.descent / 1000, // hhea writes the descent negative
|
|
862
|
+
lineGap: ttf.lineGap / 1000,
|
|
863
|
+
}
|
|
864
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.verticals();
|
|
865
|
+
if (!byStyle) {
|
|
866
|
+
byStyle = new Map();
|
|
867
|
+
this.verticalsCache.set(fontFamily, byStyle);
|
|
868
|
+
}
|
|
869
|
+
byStyle.set(fontStyle, verticals);
|
|
870
|
+
return verticals;
|
|
871
|
+
}
|
|
872
|
+
// Underline / strikethrough geometry of a face, in em fractions. Same shape as getFontVerticals:
|
|
873
|
+
// an embedded font answers from its `post`/`OS/2` tables, a standard-14 one from its AFM header,
|
|
874
|
+
// and the answer is memoised per face because it is asked once per decorated run.
|
|
875
|
+
getFontDecoration(fontFamily, fontStyle) {
|
|
876
|
+
let byStyle = this.decorationCache.get(fontFamily);
|
|
877
|
+
const hit = byStyle?.get(fontStyle);
|
|
878
|
+
if (hit)
|
|
879
|
+
return hit;
|
|
880
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
881
|
+
const decoration = ttf
|
|
882
|
+
? ttf.decoration()
|
|
883
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.decoration();
|
|
884
|
+
if (!byStyle) {
|
|
885
|
+
byStyle = new Map();
|
|
886
|
+
this.decorationCache.set(fontFamily, byStyle);
|
|
887
|
+
}
|
|
888
|
+
byStyle.set(fontStyle, decoration);
|
|
889
|
+
return decoration;
|
|
890
|
+
}
|
|
891
|
+
setKerning(enabled) {
|
|
892
|
+
this.kerningEnabled = enabled;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* The kerning adjustment for each adjacent code-point pair of `text`, in em/1000 (the sign the
|
|
896
|
+
* font declares: negative tightens). Length is `codePointCount - 1`. Zero at any pair next to a
|
|
897
|
+
* space - letters are never kerned against a space, which also keeps a per-word measure equal to a
|
|
898
|
+
* whole-line one (the space pairs contribute nothing either way).
|
|
899
|
+
*
|
|
900
|
+
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
901
|
+
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
902
|
+
*/
|
|
903
|
+
getKernPairs(text, fontFamily, fontStyle) {
|
|
904
|
+
const chars = [...text];
|
|
905
|
+
if (chars.length < 2)
|
|
906
|
+
return [];
|
|
907
|
+
// A shaped run does not kern here: these pairs are keyed by the UNSHAPED glyphs, and the `TJ`
|
|
908
|
+
// path would re-shape each chunk on its own, turning every letter isolated. Arabic kerning is in
|
|
909
|
+
// GPOS, applied during shaping - not built (`todo.md`).
|
|
910
|
+
if (this.shapeText(text, fontFamily, fontStyle))
|
|
911
|
+
return [];
|
|
912
|
+
const out = [];
|
|
913
|
+
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
914
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
915
|
+
const parser = ttf
|
|
916
|
+
? undefined
|
|
917
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser;
|
|
918
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
919
|
+
const a = chars[i];
|
|
920
|
+
const b = chars[i + 1];
|
|
921
|
+
if (a === " " || b === " ") {
|
|
922
|
+
out.push(0);
|
|
923
|
+
}
|
|
924
|
+
else if (ttf) {
|
|
925
|
+
out.push(ttf.getKerning(ttf.getGlyphIndex(a.codePointAt(0)), ttf.getGlyphIndex(b.codePointAt(0))));
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
out.push(parser.getKerning(a, b));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
935
|
+
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
936
|
+
*
|
|
937
|
+
* `bandTop` / `bandBottom` are in points relative to the baseline, y UP (so both negative under
|
|
938
|
+
* it). Only an EMBEDDED font can answer: the standard-14 outlines live in the viewer, not here.
|
|
939
|
+
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
940
|
+
*/
|
|
941
|
+
getInkSpansInBand(text, fontSize, fontFamily, fontStyle, bandTop, bandBottom, letterSpacing = 0) {
|
|
942
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
943
|
+
if (!ttf)
|
|
944
|
+
return [];
|
|
945
|
+
const scale = fontSize / ttf.unitsPerEm;
|
|
946
|
+
const spans = [];
|
|
947
|
+
let pen = 0;
|
|
948
|
+
for (const ch of text) {
|
|
949
|
+
const codePoint = ch.codePointAt(0);
|
|
950
|
+
if (codePoint !== undefined) {
|
|
951
|
+
for (const [x0, x1] of ttf.inkSpansInBand(codePoint, bandTop / scale, bandBottom / scale)) {
|
|
952
|
+
spans.push([pen + x0 * scale, pen + x1 * scale]);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// The pen advances by the glyph AND its letter-spacing, so the ink of the next glyph lands
|
|
956
|
+
// where `Tc` actually draws it - otherwise the skipped gaps drift under the wrong letters.
|
|
957
|
+
pen += this.getCharWidth(ch, fontSize, undefined, fontFamily, fontStyle) + letterSpacing;
|
|
958
|
+
}
|
|
959
|
+
return mergeSpans(spans);
|
|
960
|
+
}
|
|
542
961
|
// Returns the current width of a text, included kernings
|
|
543
962
|
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
963
|
+
// Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
|
|
964
|
+
// form while drawing the shaped one would break every line. Both sides ask `shapeText`.
|
|
965
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle);
|
|
966
|
+
if (shaped) {
|
|
967
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
968
|
+
return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
|
|
969
|
+
}
|
|
544
970
|
let width = 0;
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
971
|
+
// Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
|
|
972
|
+
// and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
|
|
973
|
+
// on code points; for BMP text this is one unit each, so the result is unchanged.
|
|
974
|
+
//
|
|
975
|
+
// NO KERNING. We write a run as a single `Tj`, and a viewer advances that by the font's plain
|
|
976
|
+
// widths - PDF never kerns on its own; a producer has to say so with a `TJ` array. Folding the
|
|
977
|
+
// AFM's kerning into the MEASUREMENT while the OUTPUT ignores it made every kerned string draw
|
|
978
|
+
// wider than its box: "AVATAR Wave" at 40pt by 19pt, "Total" at 11pt by 5.7%. Measured must equal
|
|
979
|
+
// drawn. The AFM kerning pairs stay parsed (`getKerning`) for the day we emit `TJ` - see the
|
|
980
|
+
// "real kerning" item in todo.md, which must cover embedded fonts too (`kern`/`GPOS`).
|
|
981
|
+
for (const char of text) {
|
|
982
|
+
width += this.getCharWidth(char, fontSize, undefined, fontFamily, fontStyle);
|
|
557
983
|
}
|
|
558
984
|
return width;
|
|
559
985
|
}
|
|
560
|
-
getCharCode(char) {
|
|
561
|
-
return char.charCodeAt(0).toString();
|
|
562
|
-
}
|
|
563
986
|
getAVMParserByFont(fullFontName, fontName, fontStyle) {
|
|
987
|
+
// A non-string family means font bytes were passed where a name belongs - hint at the fix instead of
|
|
988
|
+
// stringifying the byte blob into an unreadable "parser not found".
|
|
989
|
+
for (const name of [fullFontName, fontName])
|
|
990
|
+
if (name != null && typeof name !== "string")
|
|
991
|
+
throw new Error(`Font family must be a string name, got ${typeof name}. Register font bytes via the document \`fonts\` map (or addFont) and reference them by name.`);
|
|
564
992
|
if (!fullFontName && (!fontName || !fontStyle)) {
|
|
565
993
|
throw new Error("No font family is given. Please set a full font name or a font with font style");
|
|
566
994
|
}
|
|
@@ -577,26 +1005,40 @@ endstream`;
|
|
|
577
1005
|
}
|
|
578
1006
|
// Methode zur Berechnung der Zeichenbreite anhand der Schriftgröße
|
|
579
1007
|
getCharWidth(char, fontSize, fullFontName, fontName, fontStyle) {
|
|
580
|
-
const
|
|
1008
|
+
const name = fullFontName ?? fontName;
|
|
1009
|
+
const ttf = this.getCustomFont(name, fontStyle);
|
|
1010
|
+
// Color-emoji fallback: a code point the current font can't color-render is measured with the
|
|
1011
|
+
// emoji font's advance (matching the renderer, which draws it from that font). Guarded on
|
|
1012
|
+
// emojiFontName so a normal document's measuring stays byte-identical.
|
|
1013
|
+
if (this.emojiFontName && this.emojiFontName !== name) {
|
|
1014
|
+
const cp = char.codePointAt(0);
|
|
1015
|
+
if (cp !== undefined && !(ttf && this.colorRenders(ttf, cp))) {
|
|
1016
|
+
const emoji = this.getColorFont(this.emojiFontName, fontStyle);
|
|
1017
|
+
if (emoji && this.colorRenders(emoji, cp))
|
|
1018
|
+
return emoji.getCharWidth(char, fontSize);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// Image emoji source: an emoji code point is drawn as a 1em-square image, so it advances one em.
|
|
1022
|
+
if (this.emojiImage) {
|
|
1023
|
+
const cp = char.codePointAt(0);
|
|
1024
|
+
if (cp !== undefined && isEmojiCodePoint(cp) && !(ttf && this.colorRenders(ttf, cp))) {
|
|
1025
|
+
return fontSize;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
581
1028
|
if (ttf)
|
|
582
1029
|
return ttf.getCharWidth(char, fontSize);
|
|
583
1030
|
const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
|
|
584
|
-
|
|
585
|
-
//
|
|
1031
|
+
let advanceWidth = currentParser.parser.getAdvanceWidth(char);
|
|
1032
|
+
// A character a standard font cannot represent (not in Windows-1252) is drawn as "?" by the
|
|
1033
|
+
// encoder; measure it as "?" too so the width matches the glyph - graceful instead of a crash.
|
|
586
1034
|
if (!advanceWidth) {
|
|
587
|
-
|
|
1035
|
+
advanceWidth =
|
|
1036
|
+
currentParser.parser.getAdvanceWidth("?") || currentParser.parser.getAdvanceWidth(" ");
|
|
588
1037
|
}
|
|
589
1038
|
// Width of the character multiplied by the font size (scaled proportionally)
|
|
590
1039
|
return (advanceWidth / 1000) * fontSize;
|
|
591
1040
|
}
|
|
592
1041
|
// Method to get the kerning, if available, between two signs
|
|
593
|
-
getKerning(char, nextChar, fullFontName, fontName, fontStyle) {
|
|
594
|
-
// TrueType kerning (the kern/GPOS tables) is not wired up yet - no kerning for custom fonts.
|
|
595
|
-
if (this.getCustomFont(fullFontName !== null && fullFontName !== void 0 ? fullFontName : fontName, fontStyle))
|
|
596
|
-
return 0;
|
|
597
|
-
const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
|
|
598
|
-
return currentParser.parser.getKerning(char, nextChar) / 1000;
|
|
599
|
-
}
|
|
600
1042
|
// Returns all fonts
|
|
601
1043
|
getAllFontsRaw() {
|
|
602
1044
|
return this.fonts.getAllFonts();
|
|
@@ -632,14 +1074,14 @@ endstream`;
|
|
|
632
1074
|
const root = this.objects.findIndex((f) => f.toLowerCase().includes("catalog")) + 1;
|
|
633
1075
|
// A fresh document uses the same hash for both /ID strings; they only diverge on an update.
|
|
634
1076
|
const id = this.documentId ? ` /ID [${this.contentId()} ${this.contentId()}]` : "";
|
|
635
|
-
|
|
1077
|
+
const enc = this.encryptObjNum ? ` /Encrypt ${this.encryptObjNum} 0 R` : "";
|
|
1078
|
+
return `trailer\n<< /Size ${objectCount + 1} /Root ${root} 0 R${id}${enc} >>\nstartxref\n${startxref}\n%%EOF`;
|
|
636
1079
|
}
|
|
637
1080
|
contentId() {
|
|
638
|
-
return `<${(
|
|
1081
|
+
return `<${md5Hex(new TextEncoder().encode(this.objects.join(""))).toUpperCase()}>`;
|
|
639
1082
|
}
|
|
640
1083
|
// Returns the number of objects
|
|
641
1084
|
getObjectCount() {
|
|
642
1085
|
return this.objects.length;
|
|
643
1086
|
}
|
|
644
1087
|
}
|
|
645
|
-
exports.PDFObjectManager = PDFObjectManager;
|