@muhammara/native-core 7.0.0-beta.3 → 7.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -7
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/index.js +120 -1
- package/lib/PDFRStreamForBuffer.js +52 -7
- package/lib/PDFRStreamForFile.js +56 -7
- package/lib/PDFStreamForResponse.js +22 -5
- package/lib/PDFWStreamForBuffer.js +49 -10
- package/lib/PDFWStreamForFile.js +26 -5
- package/lib/Recipe.js +86 -30
- package/lib/content-stream.js +503 -0
- package/lib/css-colors.js +149 -0
- package/lib/font-text.js +1461 -0
- package/lib/glyph-list.js +1127 -0
- package/lib/recipe/annotation.js +482 -105
- package/lib/recipe/appendPage.js +8 -1
- package/lib/recipe/colors.js +132 -35
- package/lib/recipe/coordinate.js +82 -16
- package/lib/recipe/encrypt.js +32 -13
- package/lib/recipe/font.js +70 -25
- package/lib/recipe/graphics-state.js +3 -2
- package/lib/recipe/htmlToTextObjects.js +154 -21
- package/lib/recipe/image.js +34 -9
- package/lib/recipe/info.js +93 -22
- package/lib/recipe/insertPage.js +12 -3
- package/lib/recipe/overlay.js +4 -2
- package/lib/recipe/page-references.js +436 -0
- package/lib/recipe/page.js +379 -275
- package/lib/recipe/parameters.js +11 -2
- package/lib/recipe/replaceText.js +464 -30
- package/lib/recipe/shapes.js +418 -45
- package/lib/recipe/split.js +6 -2
- package/lib/recipe/table.js +260 -173
- package/lib/recipe/text.helper.js +176 -10
- package/lib/recipe/text.js +420 -115
- package/lib/recipe/utils.js +66 -3
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +150 -13
- package/lib/recipe/vector.js +150 -53
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/muhammara.d.ts +3487 -152
- package/package.json +1 -1
package/lib/recipe/parameters.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const { PAGE_CONTEXT_STATE } = require("./utils");
|
|
2
|
+
const { PageSize } = require("../recipe-constants");
|
|
3
|
+
|
|
1
4
|
this.mediumSizes = {
|
|
2
5
|
executive: [521.86, 756.0], // north american
|
|
3
6
|
folio: [612.0, 936.0],
|
|
@@ -55,6 +58,12 @@ this.mediumSizes = {
|
|
|
55
58
|
sra4: [637.8, 907.09],
|
|
56
59
|
};
|
|
57
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Initialize the page defaults (letter size, one-inch margins), the drawing
|
|
63
|
+
* position and the page-context state.
|
|
64
|
+
* @private
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
58
67
|
exports._setParameters = function _setParameters() {
|
|
59
68
|
this.debug = false;
|
|
60
69
|
this.metadata = {
|
|
@@ -62,7 +71,7 @@ exports._setParameters = function _setParameters() {
|
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
let defaultMargin = 72; // 1 inch.
|
|
65
|
-
let defaultSize =
|
|
74
|
+
let defaultSize = PageSize.LETTER;
|
|
66
75
|
|
|
67
76
|
this.default = {
|
|
68
77
|
pageSize: this.mediumSizes[defaultSize],
|
|
@@ -81,7 +90,7 @@ exports._setParameters = function _setParameters() {
|
|
|
81
90
|
};
|
|
82
91
|
|
|
83
92
|
this._margin = Object.assign({}, this.default.pageMargin);
|
|
84
|
-
this.contextState =
|
|
93
|
+
this.contextState = PAGE_CONTEXT_STATE.IDLE;
|
|
85
94
|
this.modifiedSourcePages = new Set();
|
|
86
95
|
|
|
87
96
|
// Object.assign(this, options);
|
|
@@ -1,9 +1,382 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
var contentStream = require("../content-stream");
|
|
2
|
+
var fontText = require("../font-text");
|
|
3
|
+
|
|
4
|
+
var PdfOperator = contentStream.PdfOperator;
|
|
5
|
+
var TokenKind = contentStream.TokenKind;
|
|
6
|
+
var decodeName = contentStream.decodeName;
|
|
7
|
+
var forEachOperation = contentStream.forEachOperation;
|
|
8
|
+
var tokenKind = contentStream.tokenKind;
|
|
9
|
+
|
|
10
|
+
// PDF dictionary keys and names this module reads.
|
|
11
|
+
var PdfName = Object.freeze({
|
|
12
|
+
RESOURCES: "Resources",
|
|
13
|
+
CONTENTS: "Contents",
|
|
14
|
+
LENGTH: "Length",
|
|
15
|
+
FILTER: "Filter",
|
|
16
|
+
DECODE_PARMS: "DecodeParms",
|
|
17
|
+
XOBJECT: "XObject",
|
|
18
|
+
SUBTYPE: "Subtype",
|
|
19
|
+
FORM: "Form",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Replace the operands of `Tj` operators whose text, decoded through the
|
|
24
|
+
* active font, equals `text`. Everything else is kept byte for byte.
|
|
25
|
+
*
|
|
26
|
+
* @private
|
|
27
|
+
* @param {string} source Latin-1 content stream.
|
|
28
|
+
* @param {string} text Text to replace.
|
|
29
|
+
* @param {string} replacement Replacement text.
|
|
30
|
+
* @param {function(string): object} fonts Font codec lookup by resource
|
|
31
|
+
* name.
|
|
32
|
+
* @returns {string} Latin-1 content stream.
|
|
33
|
+
* @throws {Error} If a matched font has no glyph for a replacement
|
|
34
|
+
* character.
|
|
35
|
+
*/
|
|
36
|
+
function replaceShownText(source, text, replacement, fonts) {
|
|
37
|
+
var result = "";
|
|
38
|
+
var copied = 0;
|
|
39
|
+
var font = null;
|
|
40
|
+
var fontStack = [];
|
|
41
|
+
var encoded = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Track the font and rewrite one matching `Tj` operand.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
* @param {{operator: string, operands: Array<{token: string, start:
|
|
48
|
+
* number, end: number}>}} operation Content-stream operation.
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
function replaceOperation(operation) {
|
|
52
|
+
var operator = operation.operator;
|
|
53
|
+
var operands = operation.operands;
|
|
54
|
+
if (operator === PdfOperator.SAVE_STATE) fontStack.push(font);
|
|
55
|
+
if (operator === PdfOperator.RESTORE_STATE && fontStack.length) {
|
|
56
|
+
font = fontStack.pop();
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
operator === PdfOperator.SET_FONT &&
|
|
60
|
+
operands.length === 2 &&
|
|
61
|
+
tokenKind(operands[0].token) === TokenKind.NAME
|
|
62
|
+
) {
|
|
63
|
+
font = decodeName(operands[0].token);
|
|
64
|
+
}
|
|
65
|
+
var kind = operands.length === 1 ? tokenKind(operands[0].token) : null;
|
|
66
|
+
if (
|
|
67
|
+
operator !== PdfOperator.SHOW_TEXT ||
|
|
68
|
+
(kind !== TokenKind.LITERAL_STRING && kind !== TokenKind.HEX_STRING)
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
var operand = operands[0];
|
|
73
|
+
var codec = fonts(font);
|
|
74
|
+
var bytes = contentStream.stringBytes(operand.token);
|
|
75
|
+
if (codec.decode(bytes) !== text) return;
|
|
76
|
+
if (!encoded.has(codec)) {
|
|
77
|
+
try {
|
|
78
|
+
encoded.set(codec, codec.encode(replacement));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"replaceText cannot write the replacement: " + error.message,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
result +=
|
|
86
|
+
source.slice(copied, operand.start) +
|
|
87
|
+
contentStream.stringToken(encoded.get(codec), kind);
|
|
88
|
+
copied = operand.end;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
forEachOperation(source, replaceOperation);
|
|
92
|
+
return result + source.slice(copied);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Remove text-showing operators from a page content stream. `'` and `"`
|
|
97
|
+
* keep their line advance and spacing effects as `T*`, `Tw`, and `Tc`.
|
|
98
|
+
*
|
|
99
|
+
* @private
|
|
100
|
+
* @param {string} source Latin-1 content stream.
|
|
101
|
+
* @returns {{content: string, xObjectNames: string[]}} Latin-1 content
|
|
102
|
+
* stream without shown text, and the XObject names it paints with `Do`.
|
|
103
|
+
*/
|
|
104
|
+
function removeTextShowingOperators(source) {
|
|
105
|
+
var result = "";
|
|
106
|
+
var xObjectNames = [];
|
|
107
|
+
var operandStart = 0;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Copy one operation, dropping shown text.
|
|
111
|
+
*
|
|
112
|
+
* @private
|
|
113
|
+
* @param {{operator: string, operands: Array<{token: string}>, end:
|
|
114
|
+
* number}} operation Content-stream operation.
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
function removeOperation(operation) {
|
|
118
|
+
var operator = operation.operator;
|
|
119
|
+
var operands = [];
|
|
120
|
+
for (var index = 0; index < operation.operands.length; index++) {
|
|
121
|
+
operands.push(operation.operands[index].token);
|
|
122
|
+
}
|
|
123
|
+
if (
|
|
124
|
+
operator === PdfOperator.PAINT_XOBJECT &&
|
|
125
|
+
operands.length &&
|
|
126
|
+
tokenKind(operands[0]) === TokenKind.NAME
|
|
127
|
+
) {
|
|
128
|
+
xObjectNames.push(decodeName(operands[0]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
operator === PdfOperator.SHOW_TEXT ||
|
|
133
|
+
operator === PdfOperator.SHOW_TEXT_ARRAY
|
|
134
|
+
) {
|
|
135
|
+
result += " ";
|
|
136
|
+
} else if (operator === PdfOperator.NEXT_LINE_SHOW_TEXT) {
|
|
137
|
+
result += " T*";
|
|
138
|
+
} else if (operator === PdfOperator.SPACING_NEXT_LINE_SHOW_TEXT) {
|
|
139
|
+
// `aw ac string "`; a malformed one is skipped by viewers, so drop it.
|
|
140
|
+
if (operands.length === 3)
|
|
141
|
+
result += " " + operands[0] + " Tw " + operands[1] + " Tc T*";
|
|
142
|
+
else result += " ";
|
|
143
|
+
} else {
|
|
144
|
+
result += source.slice(operandStart, operation.end);
|
|
145
|
+
}
|
|
146
|
+
operandStart = operation.end;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
forEachOperation(source, removeOperation);
|
|
150
|
+
return {
|
|
151
|
+
content: result + source.slice(operandStart),
|
|
152
|
+
xObjectNames: xObjectNames,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolve an object through an indirect reference.
|
|
158
|
+
*
|
|
159
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
160
|
+
* @param {object} object PDF object or indirect reference.
|
|
161
|
+
* @returns {object} Resolved PDF object.
|
|
162
|
+
*/
|
|
163
|
+
function resolve(recipe, object) {
|
|
164
|
+
if (
|
|
165
|
+
object &&
|
|
166
|
+
object.getType() === recipe.muhammara.ePDFObjectIndirectObjectReference
|
|
167
|
+
) {
|
|
168
|
+
return recipe.pdfReader.parseNewObject(
|
|
169
|
+
object.toPDFIndirectObjectReference().getObjectID(),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return object;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Look up a dictionary entry and resolve it.
|
|
177
|
+
*
|
|
178
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
179
|
+
* @param {object} dictionary PDF dictionary.
|
|
180
|
+
* @param {string} key Entry name.
|
|
181
|
+
* @returns {object|null} Resolved PDF object, or null when missing.
|
|
182
|
+
*/
|
|
183
|
+
function lookup(recipe, dictionary, key) {
|
|
184
|
+
return dictionary && dictionary.exists(key)
|
|
185
|
+
? resolve(recipe, dictionary.queryObject(key))
|
|
186
|
+
: null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Collect the content stream object IDs of a page, in drawing order, and the
|
|
191
|
+
* page's resources dictionary, including inherited resources.
|
|
192
|
+
*
|
|
193
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
194
|
+
* @param {number} pageIndex Zero-based page index.
|
|
195
|
+
* @private
|
|
196
|
+
* @returns {{streamIds: number[], resources: object|null}} Page content.
|
|
197
|
+
* @throws {Error} If `Contents` holds a direct stream.
|
|
198
|
+
*/
|
|
199
|
+
function pageContent(recipe, pageIndex) {
|
|
200
|
+
var muhammara = recipe.muhammara;
|
|
201
|
+
var page = recipe.pdfReader.parsePage(pageIndex).getDictionary();
|
|
202
|
+
var resources = contentStream.inheritedResources(
|
|
203
|
+
recipe.pdfReader,
|
|
204
|
+
muhammara,
|
|
205
|
+
page,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
var contents = page.exists(PdfName.CONTENTS)
|
|
209
|
+
? page.queryObject(PdfName.CONTENTS)
|
|
210
|
+
: null;
|
|
211
|
+
var resolved = resolve(recipe, contents);
|
|
212
|
+
var entries = [];
|
|
213
|
+
if (contents && resolved.getType() === muhammara.ePDFObjectArray) {
|
|
214
|
+
var array = resolved.toPDFArray();
|
|
215
|
+
for (var index = 0; index < array.getLength(); index++) {
|
|
216
|
+
entries.push(array.queryObject(index));
|
|
217
|
+
}
|
|
218
|
+
} else if (contents) {
|
|
219
|
+
entries.push(contents);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
resources: resources,
|
|
224
|
+
streamIds: entries.map(function (entry) {
|
|
225
|
+
if (entry.getType() !== muhammara.ePDFObjectIndirectObjectReference) {
|
|
226
|
+
throw new Error("removeText supports indirect page content streams");
|
|
227
|
+
}
|
|
228
|
+
return entry.toPDFIndirectObjectReference().getObjectID();
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read and decode one content stream as a Latin-1 string.
|
|
235
|
+
*
|
|
236
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
237
|
+
* @param {number} objectId Stream object ID.
|
|
238
|
+
* @returns {string} Decoded stream bytes, one character per byte.
|
|
239
|
+
*/
|
|
240
|
+
function readContentStream(recipe, objectId) {
|
|
241
|
+
return contentStream.readStreamString(
|
|
242
|
+
recipe.pdfReader,
|
|
243
|
+
recipe.pdfReader.parseNewObject(objectId).toPDFStream(),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Write a new unfiltered stream and point the page at it.
|
|
249
|
+
*
|
|
250
|
+
* @param {Recipe} recipe Recipe in modify mode.
|
|
251
|
+
* @param {number} pageIndex Zero-based page index.
|
|
252
|
+
* @param {number} objectId Object ID to replace on this page.
|
|
253
|
+
* @param {string} content Latin-1 stream content.
|
|
254
|
+
* @private
|
|
255
|
+
* @returns {void}
|
|
256
|
+
*/
|
|
257
|
+
function replaceContentStream(recipe, pageIndex, objectId, content) {
|
|
258
|
+
var objectsContext = recipe.writer.getObjectsContext();
|
|
259
|
+
var replacementObjectId = objectsContext.startNewIndirectObject();
|
|
260
|
+
var replacementStream = objectsContext.startUnfilteredPDFStream();
|
|
261
|
+
|
|
262
|
+
replacementStream.getWriteStream().write(Buffer.from(content, "latin1"));
|
|
263
|
+
objectsContext.endPDFStream(replacementStream).endIndirectObject();
|
|
264
|
+
recipe.writer.replaceObject(pageIndex, objectId, replacementObjectId);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Overwrite a source stream object in place with new unfiltered content,
|
|
269
|
+
* keeping every dictionary entry except the length and filters.
|
|
270
|
+
*
|
|
271
|
+
* @param {Recipe} recipe Recipe in modify mode.
|
|
272
|
+
* @param {object} copyingContext Copying context for the modified file.
|
|
273
|
+
* @param {number} objectId Stream object ID.
|
|
274
|
+
* @param {string} content Latin-1 stream content.
|
|
275
|
+
* @private
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
function rewriteStream(recipe, copyingContext, objectId, content) {
|
|
279
|
+
var source = recipe.pdfReader
|
|
280
|
+
.parseNewObject(objectId)
|
|
281
|
+
.toPDFStream()
|
|
282
|
+
.getDictionary()
|
|
283
|
+
.toJSObject();
|
|
284
|
+
var objectsContext = recipe.writer.getObjectsContext();
|
|
285
|
+
objectsContext.startModifiedIndirectObject(objectId);
|
|
286
|
+
var dictionary = objectsContext.startDictionary();
|
|
287
|
+
|
|
288
|
+
Object.keys(source).forEach(function (key) {
|
|
289
|
+
if (
|
|
290
|
+
key === PdfName.LENGTH ||
|
|
291
|
+
key === PdfName.FILTER ||
|
|
292
|
+
key === PdfName.DECODE_PARMS
|
|
293
|
+
)
|
|
294
|
+
return;
|
|
295
|
+
dictionary.writeKey(key);
|
|
296
|
+
copyingContext.copyDirectObjectAsIs(source[key]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
var stream = objectsContext.startUnfilteredPDFStream(dictionary);
|
|
300
|
+
stream.getWriteStream().write(Buffer.from(content, "latin1"));
|
|
301
|
+
objectsContext.endPDFStream(stream).endIndirectObject();
|
|
3
302
|
}
|
|
4
303
|
|
|
5
304
|
/**
|
|
6
|
-
*
|
|
305
|
+
* Collect the Form XObjects a content stream paints, following nested forms.
|
|
306
|
+
*
|
|
307
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
308
|
+
* @param {string[]} names XObject names used with `Do`.
|
|
309
|
+
* @param {object|null} resources Resources dictionary for those names.
|
|
310
|
+
* @param {Map<number, string>} forms Collected form IDs and stripped content.
|
|
311
|
+
* @private
|
|
312
|
+
* @returns {void}
|
|
313
|
+
*/
|
|
314
|
+
function collectForms(recipe, names, resources, forms) {
|
|
315
|
+
var xObjects = lookup(recipe, resources, PdfName.XOBJECT);
|
|
316
|
+
names.forEach(function (name) {
|
|
317
|
+
var reference =
|
|
318
|
+
xObjects && xObjects.exists(name) && xObjects.queryObject(name);
|
|
319
|
+
if (
|
|
320
|
+
!reference ||
|
|
321
|
+
reference.getType() !== recipe.muhammara.ePDFObjectIndirectObjectReference
|
|
322
|
+
) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
var objectId = reference.toPDFIndirectObjectReference().getObjectID();
|
|
326
|
+
if (forms.has(objectId)) return;
|
|
327
|
+
|
|
328
|
+
var dictionary = recipe.pdfReader
|
|
329
|
+
.parseNewObject(objectId)
|
|
330
|
+
.toPDFStream()
|
|
331
|
+
.getDictionary();
|
|
332
|
+
var subtype = lookup(recipe, dictionary, PdfName.SUBTYPE);
|
|
333
|
+
if (!subtype || subtype.toPDFName().value !== PdfName.FORM) return;
|
|
334
|
+
|
|
335
|
+
var source = readContentStream(recipe, objectId);
|
|
336
|
+
var stripped = removeTextShowingOperators(source);
|
|
337
|
+
forms.set(objectId, stripped.content === source ? null : stripped.content);
|
|
338
|
+
collectForms(
|
|
339
|
+
recipe,
|
|
340
|
+
stripped.xObjectNames,
|
|
341
|
+
lookup(recipe, dictionary, PdfName.RESOURCES) || resources,
|
|
342
|
+
forms,
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Validate a one-based page number argument.
|
|
349
|
+
*
|
|
350
|
+
* @param {Recipe} recipe Recipe with an open source reader.
|
|
351
|
+
* @param {*} pageNumber Candidate page number.
|
|
352
|
+
* @param {string} methodName Method name used in errors.
|
|
353
|
+
* @private
|
|
354
|
+
* @returns {void}
|
|
355
|
+
* @throws {TypeError} If the page number is not a positive integer.
|
|
356
|
+
* @throws {RangeError} If the source has no such page.
|
|
357
|
+
*/
|
|
358
|
+
function assertPageNumber(recipe, pageNumber, methodName) {
|
|
359
|
+
if (!Number.isInteger(pageNumber) || pageNumber < 1) {
|
|
360
|
+
throw new TypeError(methodName + " expects a positive integer page number");
|
|
361
|
+
}
|
|
362
|
+
if (!recipe.pdfReader || pageNumber > recipe.pdfReader.getPagesCount()) {
|
|
363
|
+
throw new RangeError(
|
|
364
|
+
methodName + " page " + pageNumber + " does not exist",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Replace text shown with `Tj` in a page's single content stream. Each operand
|
|
371
|
+
* is decoded through the font selected by `Tf` (its `/ToUnicode` CMap, then
|
|
372
|
+
* its `/Encoding` and `/Differences`), and a match is compared with `text`.
|
|
373
|
+
* The replacement is encoded through the same font and written back as a
|
|
374
|
+
* literal or hex string, like the original operand. Only whole `Tj` operands
|
|
375
|
+
* match; `TJ`, `'`, and `"` operands and text split across operators are left
|
|
376
|
+
* unchanged.
|
|
377
|
+
*
|
|
378
|
+
* The replacement can only use glyphs the font already has. Embedded subset
|
|
379
|
+
* fonts usually carry just the glyphs of their original text.
|
|
7
380
|
*
|
|
8
381
|
* @name replaceText
|
|
9
382
|
* @function
|
|
@@ -11,18 +384,22 @@ function escapePDFLiteralString(value) {
|
|
|
11
384
|
* @param {string} text Text to replace.
|
|
12
385
|
* @param {string} replacement Replacement text.
|
|
13
386
|
* @param {number} pageNumber One-based page number.
|
|
387
|
+
* @returns {Recipe} The Recipe instance.
|
|
388
|
+
* @throws {TypeError} If text or replacement is not a string, or if the page
|
|
389
|
+
* number is not a positive integer.
|
|
390
|
+
* @throws {RangeError} If the source document has no such page.
|
|
391
|
+
* @throws {Error} If the page does not have one indirect content stream, or
|
|
392
|
+
* the matched font cannot be read, has a malformed `/Widths` array, or has
|
|
393
|
+
* no glyph for a replacement character.
|
|
14
394
|
*/
|
|
15
395
|
exports.replaceText = function replaceText(text, replacement, pageNumber) {
|
|
16
396
|
if (typeof text !== "string" || typeof replacement !== "string") {
|
|
17
397
|
throw new TypeError("replaceText expects text and replacement strings");
|
|
18
398
|
}
|
|
19
|
-
|
|
20
|
-
throw new TypeError("replaceText expects a positive integer page number");
|
|
21
|
-
}
|
|
22
|
-
|
|
399
|
+
assertPageNumber(this, pageNumber, "replaceText");
|
|
23
400
|
var pageIndex = pageNumber - 1;
|
|
24
401
|
var page = this.pdfReader.parsePage(pageIndex).getDictionary();
|
|
25
|
-
var contents = page.queryObject(
|
|
402
|
+
var contents = page.queryObject(PdfName.CONTENTS);
|
|
26
403
|
|
|
27
404
|
if (
|
|
28
405
|
!contents ||
|
|
@@ -32,35 +409,92 @@ exports.replaceText = function replaceText(text, replacement, pageNumber) {
|
|
|
32
409
|
}
|
|
33
410
|
|
|
34
411
|
var contentsObjectId = contents.toPDFIndirectObjectReference().getObjectID();
|
|
35
|
-
var
|
|
36
|
-
var
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
var source = Buffer.concat(chunks).toString("latin1");
|
|
44
|
-
var textPattern = new RegExp(
|
|
45
|
-
"\\(" + escapePDFLiteralString(text) + "\\)(\\s+Tj\\b)",
|
|
46
|
-
"g",
|
|
47
|
-
);
|
|
48
|
-
var replaced = source.replace(
|
|
49
|
-
textPattern,
|
|
50
|
-
"(" + escapePDFLiteralString(replacement) + ")$1",
|
|
412
|
+
var source = readContentStream(this, contentsObjectId);
|
|
413
|
+
var replaced = replaceShownText(
|
|
414
|
+
source,
|
|
415
|
+
text,
|
|
416
|
+
replacement,
|
|
417
|
+
fontText.createPageFontLookup(this.pdfReader, this.muhammara, pageIndex),
|
|
51
418
|
);
|
|
52
419
|
|
|
53
420
|
if (replaced === source) {
|
|
54
421
|
return this;
|
|
55
422
|
}
|
|
56
423
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
424
|
+
replaceContentStream(this, pageIndex, contentsObjectId, replaced);
|
|
425
|
+
this.modifiedSourcePages.add(pageNumber);
|
|
426
|
+
return this;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Remove all shown text from a page, for example before placing a fresh OCR
|
|
431
|
+
* text layer. Text-showing operators (`Tj`, `TJ`, `'`, `"`) are dropped;
|
|
432
|
+
* graphics, images, and text state are kept. Annotation appearances are not
|
|
433
|
+
* changed.
|
|
434
|
+
*
|
|
435
|
+
* The page's source content streams are rewritten in place, so a stream shared
|
|
436
|
+
* with another page loses its text there too. Content added with `editPage()`
|
|
437
|
+
* in the same Recipe is kept.
|
|
438
|
+
*
|
|
439
|
+
* @name removeText
|
|
440
|
+
* @function
|
|
441
|
+
* @memberof Recipe#
|
|
442
|
+
* @param {number} pageNumber One-based page number.
|
|
443
|
+
* @param {RemoveTextOptions} [options] Removal options.
|
|
444
|
+
* @param {boolean} [options.forms=false] Also remove text from the Form
|
|
445
|
+
* XObjects the page paints, including nested forms. Other pages that paint
|
|
446
|
+
* the same form lose that text too.
|
|
447
|
+
* @returns {Recipe} The Recipe instance.
|
|
448
|
+
* @throws {TypeError} If the page number is not a positive integer, or the
|
|
449
|
+
* options are not an object.
|
|
450
|
+
* @throws {RangeError} If the source document has no such page.
|
|
451
|
+
* @throws {Error} If the page's `Contents` holds a direct stream.
|
|
452
|
+
*/
|
|
453
|
+
exports.removeText = function removeText(pageNumber, options) {
|
|
454
|
+
assertPageNumber(this, pageNumber, "removeText");
|
|
455
|
+
if (
|
|
456
|
+
options !== undefined &&
|
|
457
|
+
(options === null || typeof options !== "object")
|
|
458
|
+
) {
|
|
459
|
+
throw new TypeError("removeText expects an options object");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
var recipe = this;
|
|
463
|
+
var page = pageContent(this, pageNumber - 1);
|
|
464
|
+
var source = page.streamIds
|
|
465
|
+
.map(function (objectId) {
|
|
466
|
+
return readContentStream(recipe, objectId);
|
|
467
|
+
})
|
|
468
|
+
.join("\n");
|
|
469
|
+
var stripped = removeTextShowingOperators(source);
|
|
470
|
+
var forms = new Map();
|
|
471
|
+
if (options && options.forms) {
|
|
472
|
+
collectForms(this, stripped.xObjectNames, page.resources, forms);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Rewrite each source stream once per Recipe; the result never changes.
|
|
476
|
+
if (!this.textRemovedStreamIds) this.textRemovedStreamIds = new Set();
|
|
477
|
+
var rewritten = this.textRemovedStreamIds;
|
|
478
|
+
var copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
|
|
479
|
+
if (stripped.content !== source && !rewritten.has(page.streamIds[0])) {
|
|
480
|
+
page.streamIds.forEach(function (objectId, index) {
|
|
481
|
+
rewriteStream(
|
|
482
|
+
recipe,
|
|
483
|
+
copyingContext,
|
|
484
|
+
objectId,
|
|
485
|
+
index === 0 ? stripped.content : "",
|
|
486
|
+
);
|
|
487
|
+
rewritten.add(objectId);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
forms.forEach(function (content, objectId) {
|
|
491
|
+
if (content !== null && !rewritten.has(objectId)) {
|
|
492
|
+
rewriteStream(recipe, copyingContext, objectId, content);
|
|
493
|
+
rewritten.add(objectId);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
copyingContext.end();
|
|
60
497
|
|
|
61
|
-
replacementStream.getWriteStream().write(Array.from(Buffer.from(replaced)));
|
|
62
|
-
objectsContext.endPDFStream(replacementStream).endIndirectObject();
|
|
63
|
-
this.writer.replaceObject(pageIndex, contentsObjectId, replacementObjectId);
|
|
64
498
|
this.modifiedSourcePages.add(pageNumber);
|
|
65
499
|
return this;
|
|
66
500
|
};
|