@muhammara/native-core 7.0.0-alpha.1 → 7.0.0-beta.2
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 +4 -0
- package/index.js +45 -0
- package/lib/Recipe.js +163 -56
- package/lib/recipe/annotation.js +53 -4
- package/lib/recipe/appendPage.js +38 -16
- package/lib/recipe/graphics-state.js +20 -0
- package/lib/recipe/image.js +4 -0
- package/lib/recipe/info.js +71 -73
- package/lib/recipe/insertPage.js +20 -17
- package/lib/recipe/page.js +808 -11
- package/lib/recipe/parameters.js +2 -0
- package/lib/recipe/replaceText.js +66 -0
- package/lib/recipe/shapes.js +22 -1
- package/lib/recipe/split.js +2 -6
- package/lib/recipe/text.helper.js +1 -1
- package/lib/recipe/text.js +106 -8
- package/lib/recipe/utils.js +31 -27
- package/lib/recipe/vector-polygon.js +11 -0
- package/lib/recipe/vector.helper.js +15 -3
- package/lib/recipe/vector.js +108 -10
- package/lib/recipe-info.js +10 -0
- package/muhammara.d.ts +543 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,3 +3,7 @@
|
|
|
3
3
|
Internal shared JavaScript runtime for the MuhammaraJS native packages. Install
|
|
4
4
|
`@muhammara/native` for prebuilt binaries or `@muhammara/native-with-source` for
|
|
5
5
|
local source builds.
|
|
6
|
+
|
|
7
|
+
See the [Recipe API invariants](https://muhammarajs.readthedocs.io/latest/recipe/invariants/)
|
|
8
|
+
for behavior that is not obvious from the method signatures alone, notably the
|
|
9
|
+
direct-write behavior of file-backed `endPDF()`.
|
package/index.js
CHANGED
|
@@ -23,6 +23,51 @@ exports.createMuhammara = function createMuhammara(muhammara) {
|
|
|
23
23
|
eventParams.writer = this;
|
|
24
24
|
this.getEvents().emit(eventName, eventParams);
|
|
25
25
|
};
|
|
26
|
+
muhammara.PDFWriter.prototype.replaceObject = function (
|
|
27
|
+
pageIndex,
|
|
28
|
+
sourceObjectId,
|
|
29
|
+
replacementObjectId,
|
|
30
|
+
options,
|
|
31
|
+
) {
|
|
32
|
+
if (options && options.scope === "global") {
|
|
33
|
+
var copyingContext = this.createPDFCopyingContextForModifiedFile();
|
|
34
|
+
var pageCount = copyingContext.getSourceDocumentParser().getPagesCount();
|
|
35
|
+
|
|
36
|
+
copyingContext.end();
|
|
37
|
+
for (var index = 0; index < pageCount; ++index) {
|
|
38
|
+
this.replaceObject(index, sourceObjectId, replacementObjectId);
|
|
39
|
+
}
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var copyingContext = this.createPDFCopyingContextForModifiedFile();
|
|
44
|
+
var parser = copyingContext.getSourceDocumentParser();
|
|
45
|
+
var pageObjectId = parser.getPageObjectID(pageIndex);
|
|
46
|
+
var pageObject = parser.parsePage(pageIndex).getDictionary().toJSObject();
|
|
47
|
+
var objectsContext = this.getObjectsContext();
|
|
48
|
+
var pageDictionary;
|
|
49
|
+
|
|
50
|
+
objectsContext.startModifiedIndirectObject(pageObjectId);
|
|
51
|
+
pageDictionary = objectsContext.startDictionary();
|
|
52
|
+
|
|
53
|
+
Object.getOwnPropertyNames(pageObject).forEach(function (key) {
|
|
54
|
+
var value = pageObject[key];
|
|
55
|
+
|
|
56
|
+
pageDictionary.writeKey(key);
|
|
57
|
+
if (
|
|
58
|
+
value.getType() === muhammara.ePDFObjectIndirectObjectReference &&
|
|
59
|
+
value.toPDFIndirectObjectReference().getObjectID() === sourceObjectId
|
|
60
|
+
) {
|
|
61
|
+
pageDictionary.writeObjectReferenceValue(replacementObjectId);
|
|
62
|
+
} else {
|
|
63
|
+
copyingContext.copyDirectObjectAsIs(value);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
objectsContext.endDictionary(pageDictionary).endIndirectObject();
|
|
68
|
+
copyingContext.end();
|
|
69
|
+
return this;
|
|
70
|
+
};
|
|
26
71
|
muhammara.PDFStreamForResponse = require("./lib/PDFStreamForResponse");
|
|
27
72
|
muhammara.PDFWStreamForFile = require("./lib/PDFWStreamForFile");
|
|
28
73
|
muhammara.PDFRStreamForFile = require("./lib/PDFRStreamForFile");
|
package/lib/Recipe.js
CHANGED
|
@@ -2,6 +2,7 @@ const muhammara = require("./muhammara");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const streams = require("memory-streams");
|
|
5
|
+
var { standardInfoKeys } = require("./recipe-info");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @name Recipe
|
|
@@ -11,11 +12,11 @@ const streams = require("memory-streams");
|
|
|
11
12
|
* @param {string|Buffer} src - The file path or Buffer of the source file.
|
|
12
13
|
* @param {string} [output] - The path of the output file uses src if its not a buffer.
|
|
13
14
|
* @param {Object} [options] - The options for pdfDoc
|
|
14
|
-
* @param {number} [options.version] - The pdf version
|
|
15
|
+
* @param {number} [options.version] - The pdf version: 1.0 through 1.7 or 2.0, defaults to 1.7
|
|
15
16
|
* @param {string} [options.author] - The author
|
|
16
17
|
* @param {string} [options.title] - The title
|
|
17
18
|
* @param {string} [options.subject] - The subject
|
|
18
|
-
* @param {string} [options.colorspace] - The default colorspace: rgb, cmyk, gray
|
|
19
|
+
* @param {string} [options.colorspace] - The default colorspace: rgb, cmyk, gray, separation
|
|
19
20
|
* @param {string[]} [options.keywords] - The array of keywords
|
|
20
21
|
* @param {string} [options.password] - permission password
|
|
21
22
|
* @param {string} [options.userPassword] - this 'view' password also enables encryption
|
|
@@ -117,11 +118,15 @@ class Recipe {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
var info = {};
|
|
122
|
+
standardInfoKeys.forEach((key) => {
|
|
123
|
+
if (this.options[key] !== undefined) info[key] = this.options[key];
|
|
124
|
+
});
|
|
125
|
+
this.info(info);
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
_getVersion(version) {
|
|
124
|
-
const supportedVersions = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
|
|
129
|
+
const supportedVersions = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.0];
|
|
125
130
|
if (!supportedVersions.includes(version)) {
|
|
126
131
|
version = 1.7;
|
|
127
132
|
}
|
|
@@ -149,12 +154,14 @@ class Recipe {
|
|
|
149
154
|
*/
|
|
150
155
|
read(inSrc) {
|
|
151
156
|
const isForExternal = inSrc ? true : false;
|
|
157
|
+
let pdfReader = null;
|
|
158
|
+
let isAdopted = false;
|
|
152
159
|
try {
|
|
153
160
|
let src = isForExternal ? inSrc : this.src;
|
|
154
161
|
if (this.isBufferSrc) {
|
|
155
162
|
src = new muhammara.PDFRStreamForBuffer(this.src);
|
|
156
163
|
}
|
|
157
|
-
|
|
164
|
+
pdfReader = muhammara.createReader(src, this.encryptOptions);
|
|
158
165
|
const pages = pdfReader.getPagesCount();
|
|
159
166
|
if (pages == 0) {
|
|
160
167
|
// broken or modify password protected
|
|
@@ -206,77 +213,176 @@ class Recipe {
|
|
|
206
213
|
metadata[page.pageNumber] = page;
|
|
207
214
|
}
|
|
208
215
|
if (!isForExternal) {
|
|
216
|
+
this._releaseReader();
|
|
209
217
|
this.pdfReader = pdfReader;
|
|
210
218
|
this.metadata = metadata;
|
|
219
|
+
this.sourcePageCount = pages;
|
|
220
|
+
isAdopted = true;
|
|
211
221
|
}
|
|
212
222
|
return metadata;
|
|
213
223
|
} catch (err) {
|
|
214
224
|
throw new Error(err);
|
|
225
|
+
} finally {
|
|
226
|
+
// Only the recipe source reader outlives read(); anything else would keep
|
|
227
|
+
// the file open until the process exits.
|
|
228
|
+
if (pdfReader && !isAdopted) {
|
|
229
|
+
pdfReader.end();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Release the source reader and the file handle it holds.
|
|
236
|
+
* @private
|
|
237
|
+
* @returns {void}
|
|
238
|
+
*/
|
|
239
|
+
_releaseReader() {
|
|
240
|
+
const pdfReader = this.pdfReader;
|
|
241
|
+
if (!pdfReader) {
|
|
242
|
+
return;
|
|
215
243
|
}
|
|
244
|
+
this.pdfReader = null;
|
|
245
|
+
pdfReader.end();
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
/**
|
|
219
|
-
*
|
|
249
|
+
* Get the source reader, which endPDF() releases.
|
|
250
|
+
* @private
|
|
251
|
+
* @returns {Object} The source PDF reader.
|
|
252
|
+
* @throws {Error} If the reader was already released by endPDF().
|
|
253
|
+
*/
|
|
254
|
+
_getReader() {
|
|
255
|
+
if (!this.pdfReader) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
"The source PDF reader has been released by endPDF(). Read the source before ending the document.",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return this.pdfReader;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* End the pdfDoc. Finalization happens once; later calls do not rewrite the
|
|
265
|
+
* PDF and invoke the callback with the completed output when applicable.
|
|
266
|
+
* A failed finalization retires the Recipe and later calls rethrow the
|
|
267
|
+
* original error.
|
|
220
268
|
* @function
|
|
221
269
|
* @memberof Recipe
|
|
222
270
|
* @param {function} [callback] - The callback function.
|
|
223
271
|
* @returns {*} The callback result, if a callback is provided.
|
|
224
272
|
*/
|
|
225
273
|
endPDF(callback) {
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
(this.
|
|
232
|
-
(this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
274
|
+
if (this.endError) {
|
|
275
|
+
throw this.endError;
|
|
276
|
+
}
|
|
277
|
+
if (this.ended) {
|
|
278
|
+
if (!callback) return;
|
|
279
|
+
if (!this.isBufferSrc) return callback();
|
|
280
|
+
return callback(this.output || this.outStream.toBuffer());
|
|
281
|
+
}
|
|
282
|
+
var deletingPages = Boolean(this.deletedPages?.size);
|
|
283
|
+
var deletionState = deletingPages
|
|
284
|
+
? {
|
|
285
|
+
metadata: this.metadata,
|
|
286
|
+
metadataValues: { ...this.metadata },
|
|
287
|
+
annotations: this.annotations,
|
|
288
|
+
annotationsToWrite: this.annotationsToWrite,
|
|
289
|
+
deletedPages: this.deletedPages,
|
|
290
|
+
}
|
|
291
|
+
: null;
|
|
292
|
+
if (deletingPages && this.page) {
|
|
293
|
+
throw new Error("Finish the current page before endPDF");
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
this._deletePages();
|
|
297
|
+
this._writeInfo();
|
|
298
|
+
this.writer.end();
|
|
299
|
+
// This is a temporary work around for copying context will overwrite the current one
|
|
300
|
+
// write annotations at the end.
|
|
301
|
+
if (
|
|
302
|
+
(this.annotations && this.annotations.length > 0) ||
|
|
303
|
+
(this.annotationsToWrite && this.annotationsToWrite.length > 0)
|
|
304
|
+
) {
|
|
305
|
+
if (this.isBufferSrc) {
|
|
306
|
+
const oldStream = this.outStream;
|
|
307
|
+
this.outStream = new streams.WritableStream();
|
|
237
308
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
309
|
+
this.writer = muhammara.createWriterToModify(
|
|
310
|
+
new muhammara.PDFRStreamForBuffer(oldStream.toBuffer()),
|
|
311
|
+
new muhammara.PDFStreamForResponse(this.outStream),
|
|
312
|
+
Object.assign({}, this.encryptOptions, {
|
|
313
|
+
log: this.logFile,
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
this.writer = muhammara.createWriterToModify(
|
|
318
|
+
this.output,
|
|
319
|
+
Object.assign({}, this.encryptOptions, {
|
|
320
|
+
modifiedFilePath: this.output,
|
|
321
|
+
log: this.logFile,
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this._writeAnnotations();
|
|
327
|
+
this._writeInfo();
|
|
328
|
+
this.writer.end();
|
|
253
329
|
}
|
|
254
330
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (this.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
331
|
+
// Every step above may still read the source; _insertPages() and _encrypt()
|
|
332
|
+
// below rename the output, which is the source itself when no separate
|
|
333
|
+
// output was given, and Windows refuses that while the reader holds it.
|
|
334
|
+
this._releaseReader();
|
|
335
|
+
|
|
336
|
+
if (this.needToInsertPages) {
|
|
337
|
+
if (this.isBufferSrc) {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.log(
|
|
340
|
+
"Feature: Inserting Pages is not supported in Buffer Mode yet.",
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
this._insertPages();
|
|
344
|
+
}
|
|
267
345
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
346
|
+
if (this.needToEncrypt) {
|
|
347
|
+
if (this.isBufferSrc) {
|
|
348
|
+
// eslint-disable-next-line no-console
|
|
349
|
+
console.log(
|
|
350
|
+
"Feature: Encryption is not supported in Buffer Mode yet.",
|
|
351
|
+
);
|
|
352
|
+
} else {
|
|
353
|
+
this._encrypt();
|
|
354
|
+
}
|
|
275
355
|
}
|
|
276
|
-
}
|
|
277
356
|
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
if (this.isBufferSrc && this.output) {
|
|
358
|
+
fs.writeFileSync(this.output, this.outStream.toBuffer());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
this.ended = true;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (deletingPages) {
|
|
364
|
+
Object.keys(deletionState.metadata).forEach(
|
|
365
|
+
(key) => delete deletionState.metadata[key],
|
|
366
|
+
);
|
|
367
|
+
Object.assign(deletionState.metadata, deletionState.metadataValues);
|
|
368
|
+
this.metadata = deletionState.metadata;
|
|
369
|
+
this.annotations = deletionState.annotations;
|
|
370
|
+
this.annotationsToWrite = deletionState.annotationsToWrite;
|
|
371
|
+
this.deletedPages = deletionState.deletedPages;
|
|
372
|
+
}
|
|
373
|
+
this.endError = error;
|
|
374
|
+
this.ended = true;
|
|
375
|
+
try {
|
|
376
|
+
this.writer._abort();
|
|
377
|
+
} catch (_) {
|
|
378
|
+
// Preserve the finalization error if native cleanup also fails.
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
this._releaseReader();
|
|
382
|
+
} catch (_) {
|
|
383
|
+
// Preserve the finalization error if releasing its separate reader fails.
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
280
386
|
}
|
|
281
387
|
|
|
282
388
|
if (callback) {
|
|
@@ -302,7 +408,7 @@ class Recipe {
|
|
|
302
408
|
* @throws {string} If the callback function is unnamed when no key is provided.
|
|
303
409
|
* @throws {string} If the key conflicts with an existing Recipe prototype member.
|
|
304
410
|
* @throws {string} If the callback is not a function.
|
|
305
|
-
* @returns {
|
|
411
|
+
* @returns {Recipe} The recipe instance.
|
|
306
412
|
*/
|
|
307
413
|
register(key, callback) {
|
|
308
414
|
// Assume simply registering a function which will have an embedded name
|
|
@@ -323,11 +429,12 @@ class Recipe {
|
|
|
323
429
|
}
|
|
324
430
|
|
|
325
431
|
this.__proto__[key] = callback;
|
|
432
|
+
return this;
|
|
326
433
|
}
|
|
327
434
|
}
|
|
328
435
|
|
|
329
436
|
function loadPrototypes() {
|
|
330
|
-
const ignores = ["xObjectForm.js"];
|
|
437
|
+
const ignores = ["utils.js", "xObjectForm.js"];
|
|
331
438
|
fs.readdirSync(path.join(__dirname, "recipe"))
|
|
332
439
|
.filter((file) => {
|
|
333
440
|
return file[0] != "." && !ignores.includes(file);
|
package/lib/recipe/annotation.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* @param {string} [options.date] - The date.
|
|
12
12
|
* @param {boolean} [options.open=false] - Open the annotation by default?
|
|
13
13
|
* @param {boolean} [options.richText] - Display with rich text format, text will be transformed automatically, or you may pass in your own rich text starts with "<?xml..."
|
|
14
|
+
* @param {Array} [options.replies] - Array of annotation replies, each with text and optional title, date, subject, richText, and flag.
|
|
14
15
|
* @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
|
|
15
16
|
* @returns {Recipe} The recipe instance.
|
|
16
17
|
*/
|
|
@@ -19,17 +20,52 @@ exports.comment = function comment(text = "", x, y, options = {}) {
|
|
|
19
20
|
subtype: "Text",
|
|
20
21
|
pageNumber: this.pageNumber,
|
|
21
22
|
args: { text, x, y, options: Object.assign({ icon: "Comment" }, options) },
|
|
23
|
+
replies: options.replies,
|
|
22
24
|
});
|
|
23
25
|
return this;
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Add a clickable URL link to the current page.
|
|
30
|
+
* @name link
|
|
31
|
+
* @function
|
|
32
|
+
* @memberof Recipe#
|
|
33
|
+
* @param {string} url - The URL to open.
|
|
34
|
+
* @param {number} x - The top-left x coordinate.
|
|
35
|
+
* @param {number} y - The top-left y coordinate.
|
|
36
|
+
* @param {number} width - The link width.
|
|
37
|
+
* @param {number} height - The link height.
|
|
38
|
+
* @returns {Recipe} The recipe instance.
|
|
39
|
+
*/
|
|
40
|
+
exports.link = function link(url, x, y, width, height) {
|
|
41
|
+
const { nx, ny } = this._calibrateCoordinate(x, y, 0, -height);
|
|
42
|
+
return linkPdf(this, url, nx, ny, width, height);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function linkPdf(recipe, url, left, bottom, width, height) {
|
|
46
|
+
recipe.pauseContext();
|
|
47
|
+
try {
|
|
48
|
+
recipe.writer.attachURLLinktoCurrentPage(
|
|
49
|
+
url,
|
|
50
|
+
left,
|
|
51
|
+
bottom,
|
|
52
|
+
left + width,
|
|
53
|
+
bottom + height,
|
|
54
|
+
);
|
|
55
|
+
} finally {
|
|
56
|
+
recipe.resumeContext();
|
|
57
|
+
}
|
|
58
|
+
return recipe;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Object.defineProperty(exports, "linkPdf", { value: linkPdf });
|
|
62
|
+
|
|
26
63
|
/**
|
|
27
64
|
* Create an annotation
|
|
28
65
|
* @name annot
|
|
29
66
|
* @function
|
|
30
67
|
* @memberof Recipe#
|
|
31
68
|
* @todo support for rich text RC
|
|
32
|
-
* @todo support for opacity CA
|
|
33
69
|
* @param {number} x - The coordinate x
|
|
34
70
|
* @param {number} y - The coordinate y
|
|
35
71
|
* @param {string} subtype - The markup annotation type 'Text'|'Link'|'FreeText'|'Line'|'Square'|'Circle'|'Polygon'|'PolyLine'|'Highlight'|'Underline'|'Squiggly'|'StrikeOut'|'Caret'|'Stamp'|'Ink'|'Popup'|'FileAttachment'|'Sound'|'Movie'|'Screen'|'Widget'|'PrinterMark'|'TrapNet'|'Watermark'|'3D'|'Redact'|'Projection'|'RichMedia'
|
|
@@ -47,6 +83,7 @@ exports.comment = function comment(text = "", x, y, options = {}) {
|
|
|
47
83
|
* @param {Array} [options.replies] - Array of annotation replies
|
|
48
84
|
* @param {number} [options.border] - The border width.
|
|
49
85
|
* @param {string|number[]} [options.color] - The annotation color.
|
|
86
|
+
* @param {number} [options.opacity=1] - Annotation opacity from 0 (transparent) to 1 (opaque).
|
|
50
87
|
* @param {boolean} [options.followOriginalPageRotation=false] - Preserve the original page rotation when positioning the annotation.
|
|
51
88
|
* @returns {Recipe} The recipe instance.
|
|
52
89
|
*/
|
|
@@ -146,6 +183,11 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
|
146
183
|
.writeKey("F")
|
|
147
184
|
.writeNumberValue(getFlagBitNumberByName(params.flag));
|
|
148
185
|
|
|
186
|
+
var opacity = (reply || options).opacity ?? 1;
|
|
187
|
+
if (opacity !== 1) {
|
|
188
|
+
this.dictionaryContext.writeKey("CA").writeNumberValue(opacity);
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
/**
|
|
150
192
|
* Rich Text Strings
|
|
151
193
|
* 12.7.3.4
|
|
@@ -252,8 +294,12 @@ exports._writeAnnotations = function _writeAnnotations() {
|
|
|
252
294
|
|
|
253
295
|
if (annot.replies) {
|
|
254
296
|
annot.replies.forEach((reply) => {
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
this._annot(
|
|
298
|
+
annot.subtype,
|
|
299
|
+
{ ...annot.args, reply },
|
|
300
|
+
annot.pageNumber,
|
|
301
|
+
ref,
|
|
302
|
+
);
|
|
257
303
|
});
|
|
258
304
|
}
|
|
259
305
|
});
|
|
@@ -289,7 +335,10 @@ exports._writeAnnotation = function _writeAnnotation(pageIndex) {
|
|
|
289
335
|
objectsContext.startArray();
|
|
290
336
|
if (pageObject["Annots"] && pageObject["Annots"].toJSArray) {
|
|
291
337
|
pageObject["Annots"].toJSArray().forEach((annot) => {
|
|
292
|
-
objectsContext.writeIndirectObjectReference(
|
|
338
|
+
objectsContext.writeIndirectObjectReference(
|
|
339
|
+
annot.getObjectID(),
|
|
340
|
+
annot.getVersion(),
|
|
341
|
+
);
|
|
293
342
|
});
|
|
294
343
|
}
|
|
295
344
|
this.annotations[pageIndex].forEach((item) => {
|
package/lib/recipe/appendPage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const muhammara = require("../muhammara");
|
|
2
|
-
const
|
|
2
|
+
const utils = require("./utils");
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Append pages from the other pdf to the current pdf
|
|
@@ -7,39 +7,61 @@ const hummusUtils = require("./utils");
|
|
|
7
7
|
* @function
|
|
8
8
|
* @memberof Recipe#
|
|
9
9
|
* @param {string} pdfSrc - The path for the other pdf.
|
|
10
|
-
* @param {number|number
|
|
10
|
+
* @param {number|Array.<number|Array.<number>>} [pages=[]] - A one-based page
|
|
11
|
+
* number or array of page numbers and inclusive ranges. Omitting it appends all
|
|
12
|
+
* pages; endpoints beyond the source are clamped to its final page.
|
|
11
13
|
* @returns {Recipe} The recipe instance.
|
|
14
|
+
* @throws {RangeError} If a selection is not a positive integer or a two-value
|
|
15
|
+
* range in ascending order.
|
|
12
16
|
*/
|
|
13
17
|
exports.appendPage = function appendPage(pdfSrc, pages = []) {
|
|
14
|
-
if (
|
|
15
|
-
|
|
18
|
+
if (this.deletedPages?.size) {
|
|
19
|
+
throw new Error("appendPage cannot be combined with deletePage");
|
|
16
20
|
}
|
|
21
|
+
if (!Array.isArray(pages)) pages = [pages];
|
|
17
22
|
// Using stream so it can be closed to release reader resource (Issue #61)
|
|
18
23
|
const instream = new muhammara.PDFRStreamForFile(pdfSrc);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
let pageCount;
|
|
25
|
+
try {
|
|
26
|
+
const pdfReader = muhammara.createReader(instream);
|
|
27
|
+
try {
|
|
28
|
+
pageCount = pdfReader.getPagesCount();
|
|
29
|
+
} finally {
|
|
30
|
+
pdfReader.end();
|
|
31
|
+
}
|
|
32
|
+
} finally {
|
|
33
|
+
// An unreadable source must not leave the file open, which would keep it
|
|
34
|
+
// locked on Windows for the life of the process.
|
|
35
|
+
instream.close();
|
|
36
|
+
}
|
|
23
37
|
|
|
24
|
-
//
|
|
38
|
+
// Preserve the established upper-bound clamping after validating that the
|
|
39
|
+
// caller supplied meaningful one-based page numbers.
|
|
25
40
|
const transformPageNumber = (pageNum) => {
|
|
26
41
|
pageNum = pageNum > pageCount ? pageCount : pageNum;
|
|
27
|
-
pageNum = pageNum < 1 ? 1 : pageNum;
|
|
28
42
|
return pageNum - 1;
|
|
29
43
|
};
|
|
30
44
|
pages = pages.map((element) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
let range = Array.isArray(element) ? element : [element, element];
|
|
46
|
+
range = range.map(Number);
|
|
47
|
+
if (
|
|
48
|
+
range.length !== 2 ||
|
|
49
|
+
!range.every(
|
|
50
|
+
(pageNumber) => Number.isInteger(pageNumber) && pageNumber > 0,
|
|
51
|
+
) ||
|
|
52
|
+
range[1] < range[0]
|
|
53
|
+
) {
|
|
54
|
+
throw new RangeError("Page ranges use one-based inclusive page numbers");
|
|
35
55
|
}
|
|
56
|
+
return range.map(transformPageNumber);
|
|
36
57
|
});
|
|
37
58
|
if (pages.length > 0) {
|
|
38
|
-
|
|
59
|
+
utils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
|
|
39
60
|
specificRanges: pages,
|
|
40
61
|
});
|
|
41
62
|
} else {
|
|
42
|
-
|
|
63
|
+
utils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc);
|
|
43
64
|
}
|
|
65
|
+
this.pagesAppended = true;
|
|
44
66
|
return this;
|
|
45
67
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate subsequent content around a point in Recipe coordinates.
|
|
3
|
+
* @name rotateContent
|
|
4
|
+
* @function
|
|
5
|
+
* @memberof Recipe#
|
|
6
|
+
* @param {number} degrees - Clockwise rotation in degrees.
|
|
7
|
+
* @param {number} [x=0] - Rotation origin x coordinate.
|
|
8
|
+
* @param {number} [y=0] - Rotation origin y coordinate.
|
|
9
|
+
* @returns {Recipe} The recipe instance.
|
|
10
|
+
*/
|
|
11
|
+
exports.rotateContent = function rotateContent(degrees, x = 0, y = 0) {
|
|
12
|
+
const radians = (degrees * Math.PI) / 180;
|
|
13
|
+
const cosine = Math.cos(radians);
|
|
14
|
+
const sine = Math.sin(radians);
|
|
15
|
+
const point = this._calibrateCoordinate(x, y);
|
|
16
|
+
this.pageContext.cm(1, 0, 0, 1, point.nx, point.ny);
|
|
17
|
+
this.pageContext.cm(cosine, sine, -sine, cosine, 0, 0);
|
|
18
|
+
this.pageContext.cm(1, 0, 0, 1, -point.nx, -point.ny);
|
|
19
|
+
return this;
|
|
20
|
+
};
|
package/lib/recipe/image.js
CHANGED
|
@@ -58,6 +58,10 @@ exports.image = function image(imgSrc, x, y, options = {}) {
|
|
|
58
58
|
ctx.gs(xObject.getGsName(gsId)).drawImage(0, 0, imgSrc, imgOptions);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
if (options.link) {
|
|
62
|
+
this.link(options.link, x + offsetX, y - offsetY - height, width, height);
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
return this;
|
|
62
66
|
};
|
|
63
67
|
|