@muhammara/native-core 7.0.0-rc.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/Recipe.js +12 -10
- package/lib/recipe/image.js +357 -36
- package/lib/recipe/text.js +1 -1
- package/lib/recipe/vector.helper.js +11 -11
- package/lib/recipe/vector.js +6 -1
- package/lib/recipe-options.js +47 -0
- package/muhammara.d.ts +35 -8
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@muhammara/native)
|
|
4
4
|
[](https://github.com/julianhille/MuhammaraJS/blob/develop/LICENSE)
|
|
5
|
-
[](https://muhammarajs.readthedocs.io/)
|
|
6
6
|
|
|
7
7
|
**This is an internal package. Do not install it directly.**
|
|
8
8
|
|
package/lib/Recipe.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const muhammara = require("./muhammara");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const
|
|
4
|
+
const PDFWStreamForBuffer = require("./PDFWStreamForBuffer");
|
|
5
5
|
var { standardInfoKeys } = require("./recipe-info");
|
|
6
6
|
var { AnnotSubtype, PageLayout, Source } = require("./recipe-constants");
|
|
7
7
|
|
|
@@ -53,7 +53,7 @@ class Recipe {
|
|
|
53
53
|
);
|
|
54
54
|
|
|
55
55
|
if (this.isBufferSrc) {
|
|
56
|
-
this.outStream = new
|
|
56
|
+
this.outStream = new PDFWStreamForBuffer();
|
|
57
57
|
this.output = output;
|
|
58
58
|
} else {
|
|
59
59
|
this.output = output || src;
|
|
@@ -109,7 +109,7 @@ class Recipe {
|
|
|
109
109
|
);
|
|
110
110
|
} else {
|
|
111
111
|
this.writer = muhammara.createWriter(
|
|
112
|
-
|
|
112
|
+
this.outStream,
|
|
113
113
|
Object.assign({}, this.encryptOptions, {
|
|
114
114
|
version: this._getVersion(this.options.version),
|
|
115
115
|
log: this.logFile,
|
|
@@ -122,7 +122,7 @@ class Recipe {
|
|
|
122
122
|
if (this.isBufferSrc) {
|
|
123
123
|
this.writer = muhammara.createWriterToModify(
|
|
124
124
|
new muhammara.PDFRStreamForBuffer(this.src),
|
|
125
|
-
|
|
125
|
+
this.outStream,
|
|
126
126
|
Object.assign({}, this.encryptOptions, {
|
|
127
127
|
log: this.logFile,
|
|
128
128
|
}),
|
|
@@ -319,7 +319,7 @@ class Recipe {
|
|
|
319
319
|
if (this.ended) {
|
|
320
320
|
if (!callback) return;
|
|
321
321
|
if (!this.isBufferSrc) return callback();
|
|
322
|
-
return callback(this.output || this.outStream.
|
|
322
|
+
return callback(this.output || this.outStream.buffer || Buffer.alloc(0));
|
|
323
323
|
}
|
|
324
324
|
var deletingPages = Boolean(this.deletedPages?.size);
|
|
325
325
|
var deletionState = deletingPages
|
|
@@ -350,11 +350,13 @@ class Recipe {
|
|
|
350
350
|
) {
|
|
351
351
|
if (this.isBufferSrc) {
|
|
352
352
|
const oldStream = this.outStream;
|
|
353
|
-
this.outStream = new
|
|
353
|
+
this.outStream = new PDFWStreamForBuffer();
|
|
354
354
|
|
|
355
355
|
this.writer = muhammara.createWriterToModify(
|
|
356
|
-
new muhammara.PDFRStreamForBuffer(
|
|
357
|
-
|
|
356
|
+
new muhammara.PDFRStreamForBuffer(
|
|
357
|
+
oldStream.buffer || Buffer.alloc(0),
|
|
358
|
+
),
|
|
359
|
+
this.outStream,
|
|
358
360
|
Object.assign({}, this.encryptOptions, {
|
|
359
361
|
log: this.logFile,
|
|
360
362
|
}),
|
|
@@ -401,7 +403,7 @@ class Recipe {
|
|
|
401
403
|
}
|
|
402
404
|
|
|
403
405
|
if (this.isBufferSrc && this.output) {
|
|
404
|
-
fs.writeFileSync(this.output, this.outStream.
|
|
406
|
+
fs.writeFileSync(this.output, this.outStream.buffer || Buffer.alloc(0));
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
this.ended = true;
|
|
@@ -436,7 +438,7 @@ class Recipe {
|
|
|
436
438
|
if (this.output) {
|
|
437
439
|
return callback(this.output);
|
|
438
440
|
} else {
|
|
439
|
-
return callback(this.outStream.
|
|
441
|
+
return callback(this.outStream.buffer || Buffer.alloc(0));
|
|
440
442
|
}
|
|
441
443
|
} else {
|
|
442
444
|
return callback();
|
package/lib/recipe/image.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
1
2
|
const muhammara = require("../muhammara");
|
|
2
3
|
const { HorizontalAlign, VerticalAlign } = require("../recipe-constants");
|
|
3
4
|
|
|
@@ -11,26 +12,60 @@ const { HorizontalAlign, VerticalAlign } = require("../recipe-constants");
|
|
|
11
12
|
* @param {number|"center"} y - The coordinate y of the top-left corner
|
|
12
13
|
* @param {Object} [options] - The options
|
|
13
14
|
* @returns {Recipe} The recipe instance.
|
|
14
|
-
* @param {number} [options.
|
|
15
|
+
* @param {number} [options.page=1] - The one-based page of a PDF source, or the
|
|
16
|
+
* image of a multi-image TIFF, as in `overlay()`.
|
|
17
|
+
* @param {number} [options.width] - The new width. The frame's line width is
|
|
18
|
+
* `options.lineWidth`.
|
|
15
19
|
* @param {number} [options.height] - The new height
|
|
16
20
|
* @param {number} [options.scale] - Scale the image from the original width and height.
|
|
17
21
|
* @param {boolean} [options.keepAspectRatio=true] - Keep the aspect ratio.
|
|
18
|
-
* @param {number} [options.opacity] - The opacity.
|
|
22
|
+
* @param {number} [options.opacity] - The opacity of the image and its frame.
|
|
19
23
|
* @param {string} [options.align] - A `Recipe.HorizontalAlign` value, optionally
|
|
20
24
|
* followed by a space and a `Recipe.VerticalAlign` value, for example
|
|
21
25
|
* "center center". Horizontal center moves the image left by half its width
|
|
22
26
|
* and right moves it right by half; vertical center moves it up by half its
|
|
23
27
|
* height and bottom moves it down by half from its top-left placement.
|
|
28
|
+
* @param {number} [options.rotation] - Rotate the image, in degrees.
|
|
29
|
+
* @param {number[]} [options.rotationOrigin] - [x, y] of the rotation origin;
|
|
30
|
+
* the bottom-left corner of the drawn image when omitted.
|
|
31
|
+
* @param {number} [options.skewX] - Skew angle off the x axis, in degrees.
|
|
32
|
+
* @param {number} [options.skewY] - Skew angle off the y axis, in degrees.
|
|
33
|
+
* @param {string|number[]} [options.fill] - Paint the image box beneath the
|
|
34
|
+
* image in this color.
|
|
35
|
+
* @param {string|number[]} [options.stroke] - Outline the image box above the
|
|
36
|
+
* image in this color.
|
|
37
|
+
* @param {string|number[]} [options.color] - Outline color when `stroke` is
|
|
38
|
+
* not given.
|
|
39
|
+
* @param {Recipe.Colorspace} [options.colorspace] - The frame colorspace.
|
|
40
|
+
* @param {string} [options.colorName] - The Separation ink name of a
|
|
41
|
+
* `"separation"` frame color.
|
|
42
|
+
* @param {number} [options.lineWidth] - The outline width. The outline lies
|
|
43
|
+
* inside the image box, as a `rectangle()` stroke does.
|
|
44
|
+
* @param {number[]} [options.dash] - The outline dash pattern.
|
|
45
|
+
* @param {number} [options.dashPhase] - The outline dash phase.
|
|
46
|
+
* @param {Recipe.LineCap} [options.lineCap] - The outline dash cap.
|
|
47
|
+
* @param {Recipe.LineJoin} [options.lineJoin] - The outline corner join.
|
|
48
|
+
* @param {number} [options.miterLimit] - The outline miter limit.
|
|
49
|
+
* @param {boolean|number} [options.debug] - Outline the image box in green and
|
|
50
|
+
* mark the placement point in red.
|
|
24
51
|
* @param {string} [options.link] - Make the image open this URL.
|
|
25
52
|
* @throws {TypeError} If no page is active.
|
|
26
|
-
* @throws {
|
|
53
|
+
* @throws {RangeError} If `page` is not an integer from 1 to 4294967296,
|
|
54
|
+
* a size is not a finite number, or `miterLimit` is not a number of at
|
|
55
|
+
* least 1.
|
|
56
|
+
* @throws {TypeError} If `rotation` is not a finite number.
|
|
57
|
+
* @throws {Error} If the image, or the PDF page `page` selects, cannot be read.
|
|
27
58
|
*/
|
|
28
59
|
exports.image = function image(imgSrc, x, y, options = {}) {
|
|
29
|
-
|
|
60
|
+
// null options act like omitted options.
|
|
61
|
+
if (options === null) options = {};
|
|
62
|
+
const index = imageIndex(options);
|
|
63
|
+
const { width, height, offsetX, offsetY, page } = this._getImgOffset(
|
|
30
64
|
imgSrc,
|
|
31
65
|
options,
|
|
32
66
|
);
|
|
33
67
|
const imgOptions = {
|
|
68
|
+
index,
|
|
34
69
|
transformation: {
|
|
35
70
|
fit: muhammara.ImageFit.ALWAYS,
|
|
36
71
|
// proportional: true,
|
|
@@ -42,7 +77,13 @@ exports.image = function image(imgSrc, x, y, options = {}) {
|
|
|
42
77
|
|
|
43
78
|
const _options = this._getPathOptions(options, nx, ny);
|
|
44
79
|
const gsId = _options.fillGsId;
|
|
45
|
-
const xObjectKeyName = `${imgSrc}__${gsId}`;
|
|
80
|
+
const xObjectKeyName = `${imgSrc}__${index}__${gsId}`;
|
|
81
|
+
|
|
82
|
+
// `width` is the image width; only `lineWidth` sizes the frame outline.
|
|
83
|
+
const frame = { ...options, width: undefined };
|
|
84
|
+
if (options.fill) {
|
|
85
|
+
drawFrame(this, nx, ny, width, height, { ...frame, stroke: undefined });
|
|
86
|
+
}
|
|
46
87
|
|
|
47
88
|
// See if this image has been seen already, so as to not duplicate it.
|
|
48
89
|
let xObject = this.xObjects.find((element) => {
|
|
@@ -65,61 +106,339 @@ exports.image = function image(imgSrc, x, y, options = {}) {
|
|
|
65
106
|
xObject.set("height", height);
|
|
66
107
|
|
|
67
108
|
this.xObjects.push(xObject);
|
|
68
|
-
ctx.gs(xObject.getGsName(gsId))
|
|
109
|
+
ctx.gs(xObject.getGsName(gsId));
|
|
110
|
+
if (page) {
|
|
111
|
+
const placed = placedPage(page, width, height);
|
|
112
|
+
ctx.cm(...placed.matrix).drawImage(placed.x, placed.y, imgSrc, {
|
|
113
|
+
index,
|
|
114
|
+
transformation: { ...imgOptions.transformation, ...placed.size },
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
ctx.drawImage(0, 0, imgSrc, imgOptions);
|
|
118
|
+
}
|
|
69
119
|
});
|
|
70
120
|
|
|
121
|
+
if (options.stroke || options.color) {
|
|
122
|
+
drawFrame(this, nx, ny, width, height, { ...frame, fill: undefined });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.debug) {
|
|
126
|
+
drawFrame(this, nx, ny, width, height, {
|
|
127
|
+
stroke: DEBUG_OUTLINE,
|
|
128
|
+
lineWidth: 1,
|
|
129
|
+
rotation: options.rotation,
|
|
130
|
+
rotationOrigin: options.rotationOrigin,
|
|
131
|
+
skewX: options.skewX,
|
|
132
|
+
skewY: options.skewY,
|
|
133
|
+
});
|
|
134
|
+
this.circle(x, y, 2, { fill: DEBUG_ANCHOR });
|
|
135
|
+
}
|
|
136
|
+
|
|
71
137
|
if (options.link) {
|
|
72
|
-
this.
|
|
138
|
+
const [linkX, linkY] = this._centrify(x, y);
|
|
139
|
+
this.link(
|
|
140
|
+
options.link,
|
|
141
|
+
linkX + offsetX,
|
|
142
|
+
linkY - offsetY - height,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
);
|
|
73
146
|
}
|
|
74
147
|
|
|
75
148
|
return this;
|
|
76
149
|
};
|
|
77
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Read the one-based `page` option as the zero-based index of the PDF page or
|
|
153
|
+
* TIFF image that PDFWriter takes.
|
|
154
|
+
* @private
|
|
155
|
+
* @param {Object} options - The image() options.
|
|
156
|
+
* @returns {number} The zero-based index; 0 when `page` is omitted.
|
|
157
|
+
* @throws {RangeError} If `page` is not an integer from 1 to 4294967296.
|
|
158
|
+
*/
|
|
159
|
+
function imageIndex(options) {
|
|
160
|
+
const page = options.page ?? 1;
|
|
161
|
+
if (!Number.isInteger(page) || page < 1 || page > 0x100000000) {
|
|
162
|
+
throw new RangeError("image page must be an integer from 1 to 4294967296");
|
|
163
|
+
}
|
|
164
|
+
return page - 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Read a size option of `image()` as a number. A numeric string counts as its
|
|
169
|
+
* number, as in Wasm; a missing or other falsy value means not given.
|
|
170
|
+
* A negative size mirrors the image.
|
|
171
|
+
* @private
|
|
172
|
+
* @param {Object} options - The image() options.
|
|
173
|
+
* @param {string} key - `width`, `height`, or `scale`.
|
|
174
|
+
* @returns {number|undefined} The size, or undefined when not given.
|
|
175
|
+
* @throws {RangeError} If the value is not a finite number.
|
|
176
|
+
*/
|
|
177
|
+
function imageSize(options, key) {
|
|
178
|
+
if (!options[key]) return undefined;
|
|
179
|
+
const size = Number(options[key]);
|
|
180
|
+
if (!Number.isFinite(size)) {
|
|
181
|
+
throw new RangeError(`image ${key} must be a finite number`);
|
|
182
|
+
}
|
|
183
|
+
return size;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read the media box and rotation of a page of a PDF image source, caching
|
|
188
|
+
* them per file version on the Recipe. Returns null for a source that is not
|
|
189
|
+
* a readable PDF file, which is measured and drawn as an image.
|
|
190
|
+
* @private
|
|
191
|
+
* @param {Recipe} recipe - The recipe instance.
|
|
192
|
+
* @param {string} imgSrc - The image path.
|
|
193
|
+
* @param {number} index - The zero-based page.
|
|
194
|
+
* @returns {?{mediaBox: number[], rotate: number}} The page geometry.
|
|
195
|
+
* @throws {Error} If the PDF has no page at `index`: PDFWriter would read a
|
|
196
|
+
* page that does not exist.
|
|
197
|
+
*/
|
|
198
|
+
function pdfPage(recipe, imgSrc, index) {
|
|
199
|
+
let key;
|
|
200
|
+
try {
|
|
201
|
+
const stat = fs.statSync(imgSrc);
|
|
202
|
+
key = `${imgSrc}\0${stat.mtimeMs}\0${stat.size}`;
|
|
203
|
+
} catch {
|
|
204
|
+
// A missing file measures 0 by 0 and is rejected as an unknown image.
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
recipe._pdfSources = recipe._pdfSources || new Map();
|
|
208
|
+
let source = recipe._pdfSources.get(key);
|
|
209
|
+
if (!source) {
|
|
210
|
+
source = {
|
|
211
|
+
pdf: recipe.writer.getImageType(imgSrc) === muhammara.PDFImageType.PDF,
|
|
212
|
+
pages: new Map(),
|
|
213
|
+
};
|
|
214
|
+
recipe._pdfSources.set(key, source);
|
|
215
|
+
}
|
|
216
|
+
if (!source.pdf) return null;
|
|
217
|
+
if (!source.pages.has(index)) {
|
|
218
|
+
source.pages.set(index, readPdfPage(imgSrc, index));
|
|
219
|
+
}
|
|
220
|
+
const page = source.pages.get(index);
|
|
221
|
+
if (!page) throw new Error(`Unknown image: ${imgSrc}`);
|
|
222
|
+
return page;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Read the media box and rotation of one page of a PDF file.
|
|
227
|
+
* @private
|
|
228
|
+
* @param {string} imgSrc - The PDF path.
|
|
229
|
+
* @param {number} index - The zero-based page.
|
|
230
|
+
* @returns {?{mediaBox: number[], rotate: number}} The page geometry, or null
|
|
231
|
+
* when the PDF cannot be read or has no such page.
|
|
232
|
+
*/
|
|
233
|
+
function readPdfPage(imgSrc, index) {
|
|
234
|
+
let reader;
|
|
235
|
+
try {
|
|
236
|
+
reader = muhammara.createReader(imgSrc);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
if (index >= reader.getPagesCount()) return null;
|
|
242
|
+
const page = reader.parsePage(index);
|
|
243
|
+
return { mediaBox: page.getMediaBox(), rotate: page.getRotate() };
|
|
244
|
+
} finally {
|
|
245
|
+
reader.end();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The size of a PDF page as it is displayed: its media box, turned by its
|
|
251
|
+
* `/Rotate`.
|
|
252
|
+
* @private
|
|
253
|
+
* @param {{mediaBox: number[], rotate: number}} page - The page geometry.
|
|
254
|
+
* @returns {{width: number, height: number}} The displayed size.
|
|
255
|
+
*/
|
|
256
|
+
function displayedPageSize(page) {
|
|
257
|
+
const [left, bottom, right, top] = page.mediaBox;
|
|
258
|
+
return quarterTurns(page.rotate) % 2
|
|
259
|
+
? { width: top - bottom, height: right - left }
|
|
260
|
+
: { width: right - left, height: top - bottom };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* How to draw a PDF page into a displayed box: PDFWriter fits the unturned
|
|
265
|
+
* media box and ignores its origin, so the page is turned by its `/Rotate`
|
|
266
|
+
* and drawn at its origin scaled away.
|
|
267
|
+
* @private
|
|
268
|
+
* @param {{mediaBox: number[], rotate: number}} page - The page geometry.
|
|
269
|
+
* @param {number} width - The displayed width.
|
|
270
|
+
* @param {number} height - The displayed height.
|
|
271
|
+
* @returns {{matrix: number[], x: number, y: number, size: {width: number, height: number}}}
|
|
272
|
+
* The turning matrix, the drawing position, and the fitted unturned size.
|
|
273
|
+
*/
|
|
274
|
+
function placedPage(page, width, height) {
|
|
275
|
+
const [left, bottom, right, top] = page.mediaBox;
|
|
276
|
+
const turns = quarterTurns(page.rotate);
|
|
277
|
+
const w = turns % 2 ? height : width;
|
|
278
|
+
const h = turns % 2 ? width : height;
|
|
279
|
+
const matrix = [
|
|
280
|
+
[1, 0, 0, 1, 0, 0],
|
|
281
|
+
[0, -1, 1, 0, 0, w],
|
|
282
|
+
[-1, 0, 0, -1, w, h],
|
|
283
|
+
[0, 1, -1, 0, h, 0],
|
|
284
|
+
][turns];
|
|
285
|
+
return {
|
|
286
|
+
matrix,
|
|
287
|
+
x: (-left * w) / (right - left),
|
|
288
|
+
y: (-bottom * h) / (top - bottom),
|
|
289
|
+
size: { width: w, height: h },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The clockwise quarter turns of a page `/Rotate` value.
|
|
295
|
+
* @private
|
|
296
|
+
* @param {number} rotate - The `/Rotate` value, a multiple of 90.
|
|
297
|
+
* @returns {number} 0 to 3; 0 for a value that is not a multiple of 90.
|
|
298
|
+
*/
|
|
299
|
+
function quarterTurns(rotate) {
|
|
300
|
+
const turns = (((Number(rotate) || 0) % 360) + 360) % 360;
|
|
301
|
+
return turns % 90 ? 0 : turns / 90;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The diagnostic colors the shape `debug` options draw with.
|
|
305
|
+
const DEBUG_OUTLINE = "#00ff00";
|
|
306
|
+
const DEBUG_ANCHOR = "#ff0000";
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Paint the box of a placed image, with the image's rotation and skew: a fill
|
|
310
|
+
* when `options.fill` is given, otherwise an outline inset by half its line
|
|
311
|
+
* width, as `rectangle()` strokes are.
|
|
312
|
+
* @private
|
|
313
|
+
* @param {Recipe} recipe - The recipe instance.
|
|
314
|
+
* @param {number} nx - The PDF x of the image's bottom-left corner.
|
|
315
|
+
* @param {number} ny - The PDF y of the image's bottom-left corner.
|
|
316
|
+
* @param {number} width - The drawn image width.
|
|
317
|
+
* @param {number} height - The drawn image height.
|
|
318
|
+
* @param {Object} options - The image() frame options, `width` excluded.
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
321
|
+
function drawFrame(recipe, nx, ny, width, height, options) {
|
|
322
|
+
const pathOptions = recipe._getPathOptions(options, nx, ny);
|
|
323
|
+
// A negative size mirrors the image into the other side of its corner.
|
|
324
|
+
const left = Math.min(0, width);
|
|
325
|
+
const bottom = Math.min(0, height);
|
|
326
|
+
const boxWidth = Math.abs(width);
|
|
327
|
+
const boxHeight = Math.abs(height);
|
|
328
|
+
if (options.fill) {
|
|
329
|
+
const colorModel = pathOptions.fillModel;
|
|
330
|
+
pathOptions.type = muhammara.DrawingPathType.FILL;
|
|
331
|
+
pathOptions.color = colorModel.color;
|
|
332
|
+
pathOptions.colorspace = colorModel.colorspace;
|
|
333
|
+
recipe._drawObject(
|
|
334
|
+
recipe,
|
|
335
|
+
nx,
|
|
336
|
+
ny,
|
|
337
|
+
width,
|
|
338
|
+
height,
|
|
339
|
+
pathOptions,
|
|
340
|
+
(ctx, xObject) => {
|
|
341
|
+
ctx.gs(xObject.getGsName(pathOptions.fillGsId));
|
|
342
|
+
xObject.fill(colorModel);
|
|
343
|
+
ctx.drawRectangle(
|
|
344
|
+
left,
|
|
345
|
+
bottom,
|
|
346
|
+
boxWidth,
|
|
347
|
+
boxHeight,
|
|
348
|
+
recipe._devicePathOptions(pathOptions),
|
|
349
|
+
);
|
|
350
|
+
},
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const colorModel =
|
|
355
|
+
pathOptions.stroke !== undefined
|
|
356
|
+
? pathOptions.strokeModel
|
|
357
|
+
: pathOptions.colorModel;
|
|
358
|
+
pathOptions.type = muhammara.DrawingPathType.STROKE;
|
|
359
|
+
pathOptions.color = colorModel.color;
|
|
360
|
+
pathOptions.colorspace = colorModel.colorspace;
|
|
361
|
+
recipe._drawObject(
|
|
362
|
+
recipe,
|
|
363
|
+
nx,
|
|
364
|
+
ny,
|
|
365
|
+
width,
|
|
366
|
+
height,
|
|
367
|
+
pathOptions,
|
|
368
|
+
(ctx, xObject) => {
|
|
369
|
+
// The form clips at the image box, so the outline lies inside it.
|
|
370
|
+
const margin = pathOptions.width;
|
|
371
|
+
ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
|
|
372
|
+
xObject.stroke(colorModel);
|
|
373
|
+
ctx
|
|
374
|
+
.J(pathOptions.lineCap)
|
|
375
|
+
.j(pathOptions.lineJoin)
|
|
376
|
+
.M(pathOptions.miterLimit)
|
|
377
|
+
.d(pathOptions.dash, pathOptions.dashPhase)
|
|
378
|
+
.drawRectangle(
|
|
379
|
+
left + Math.min(margin / 2, boxWidth / 2),
|
|
380
|
+
bottom + Math.min(margin / 2, boxHeight / 2),
|
|
381
|
+
Math.max(0, boxWidth - margin),
|
|
382
|
+
Math.max(0, boxHeight - margin),
|
|
383
|
+
recipe._devicePathOptions(pathOptions),
|
|
384
|
+
);
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
78
389
|
/**
|
|
79
390
|
* Compute the drawn size of an image and the offset its alignment applies.
|
|
80
|
-
*
|
|
391
|
+
* A missing `options.keepAspectRatio` keeps the aspect ratio.
|
|
81
392
|
* @private
|
|
82
393
|
* @param {string} [imgSrc=''] - The image path.
|
|
83
|
-
* @param {Object} [options] - The image() options
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
394
|
+
* @param {Object} [options] - The image() options; `page` selects the page or
|
|
395
|
+
* image that is measured.
|
|
396
|
+
* @returns {{width: number, height: number, offsetX: number, offsetY: number, page: ?Object}}
|
|
397
|
+
* The drawn size, the PDF offset from the placement point, and the page
|
|
398
|
+
* geometry of a PDF source.
|
|
399
|
+
* @throws {Error} If the image, or the PDF page `page` selects, cannot be read.
|
|
87
400
|
*/
|
|
88
401
|
exports._getImgOffset = function _getImgOffset(imgSrc = "", options = {}) {
|
|
89
|
-
//
|
|
90
|
-
options.keepAspectRatio
|
|
91
|
-
|
|
92
|
-
const
|
|
402
|
+
// A missing keepAspectRatio keeps it; the caller's options stay untouched.
|
|
403
|
+
const keepAspectRatio = options.keepAspectRatio ?? true;
|
|
404
|
+
const index = imageIndex(options);
|
|
405
|
+
const page = pdfPage(this, imgSrc, index);
|
|
406
|
+
const dimensions = page
|
|
407
|
+
? displayedPageSize(page)
|
|
408
|
+
: this.writer.getImageDimensions(imgSrc, index);
|
|
93
409
|
// An unreadable image measures 0 by 0. Drawing it would leave an image
|
|
94
410
|
// object that is never written, so endPDF() could not finish the PDF.
|
|
95
411
|
if (!(dimensions.width > 0 && dimensions.height > 0)) {
|
|
96
412
|
throw new Error(`Unknown image: ${imgSrc}`);
|
|
97
413
|
}
|
|
98
414
|
const ratio = dimensions.width / dimensions.height;
|
|
415
|
+
const scale = imageSize(options, "scale");
|
|
416
|
+
const wantedWidth = imageSize(options, "width");
|
|
417
|
+
const wantedHeight = imageSize(options, "height");
|
|
99
418
|
|
|
100
419
|
let width = dimensions.width;
|
|
101
420
|
let height = dimensions.height;
|
|
102
|
-
if (
|
|
103
|
-
width = width *
|
|
104
|
-
height = height *
|
|
105
|
-
} else if (
|
|
106
|
-
width =
|
|
107
|
-
height =
|
|
108
|
-
} else if (!
|
|
109
|
-
width =
|
|
110
|
-
height =
|
|
111
|
-
} else if (
|
|
112
|
-
if (!
|
|
113
|
-
width =
|
|
114
|
-
height =
|
|
421
|
+
if (scale) {
|
|
422
|
+
width = width * scale;
|
|
423
|
+
height = height * scale;
|
|
424
|
+
} else if (wantedWidth && !wantedHeight) {
|
|
425
|
+
width = wantedWidth;
|
|
426
|
+
height = wantedWidth / ratio;
|
|
427
|
+
} else if (!wantedWidth && wantedHeight) {
|
|
428
|
+
width = wantedHeight * ratio;
|
|
429
|
+
height = wantedHeight;
|
|
430
|
+
} else if (wantedWidth && wantedHeight) {
|
|
431
|
+
if (!keepAspectRatio) {
|
|
432
|
+
width = wantedWidth;
|
|
433
|
+
height = wantedHeight;
|
|
115
434
|
} else {
|
|
116
435
|
// fit to the smaller
|
|
117
|
-
if (
|
|
118
|
-
width =
|
|
119
|
-
height =
|
|
436
|
+
if (wantedWidth / ratio <= wantedHeight) {
|
|
437
|
+
width = wantedWidth;
|
|
438
|
+
height = wantedWidth / ratio;
|
|
120
439
|
} else {
|
|
121
|
-
width =
|
|
122
|
-
height =
|
|
440
|
+
width = wantedHeight * ratio;
|
|
441
|
+
height = wantedHeight;
|
|
123
442
|
}
|
|
124
443
|
}
|
|
125
444
|
}
|
|
@@ -145,11 +464,13 @@ exports._getImgOffset = function _getImgOffset(imgSrc = "", options = {}) {
|
|
|
145
464
|
offsetY = (-1 * height) / 2;
|
|
146
465
|
break;
|
|
147
466
|
case VerticalAlign.BOTTOM:
|
|
148
|
-
|
|
467
|
+
// Down by half the height from the top-left placement, as
|
|
468
|
+
// documented; 6.x moved the image up by 1.5 times its height.
|
|
469
|
+
offsetY = -1.5 * height;
|
|
149
470
|
break;
|
|
150
471
|
default:
|
|
151
472
|
}
|
|
152
473
|
}
|
|
153
474
|
}
|
|
154
|
-
return { width, height, offsetX, offsetY };
|
|
475
|
+
return { width, height, offsetX, offsetY, page };
|
|
155
476
|
};
|
package/lib/recipe/text.js
CHANGED
|
@@ -321,7 +321,7 @@ exports._makeTextBox = function _makeTextBox(options) {
|
|
|
321
321
|
* @param {string} [options.subject] - Subject of annotation.
|
|
322
322
|
* @param {string} [options.link] - Make the text open this URL.
|
|
323
323
|
* @returns {Recipe} The recipe instance. Without an active page nothing is drawn.
|
|
324
|
-
* @throws {TypeError} If `options.charSpace` is not a finite number; nothing is drawn.
|
|
324
|
+
* @throws {TypeError} If `options.charSpace` or `options.rotation` is not a finite number; nothing is drawn.
|
|
325
325
|
* @throws {Error} If an overflow callback names an undefined layout, or a font cannot be loaded.
|
|
326
326
|
*/
|
|
327
327
|
exports.text = function text(text = "", x, y, options = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { xObjectForm } = require("./xObjectForm");
|
|
2
2
|
const { resolveFontSize } = require("./utils");
|
|
3
3
|
const { Colorspace, LineCap, LineJoin } = require("../recipe-constants");
|
|
4
|
+
const { miterLimitOption, rotationOption } = require("../recipe-options");
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolve drawing and text options into path options: font, size, colors
|
|
@@ -11,7 +12,9 @@ const { Colorspace, LineCap, LineJoin } = require("../recipe-constants");
|
|
|
11
12
|
* @param {number} originX - The PDF x of the default rotation origin.
|
|
12
13
|
* @param {number} originY - The PDF y of the default rotation origin.
|
|
13
14
|
* @returns {Object} The resolved path options.
|
|
14
|
-
* @throws {RangeError} If a given font size is not greater than zero
|
|
15
|
+
* @throws {RangeError} If a given font size is not greater than zero, or
|
|
16
|
+
* `miterLimit` is not a number of at least 1.
|
|
17
|
+
* @throws {TypeError} If `rotation` is not a finite number.
|
|
15
18
|
* @throws {Error} If the font cannot be loaded.
|
|
16
19
|
*/
|
|
17
20
|
exports._getPathOptions = function _getPathOptions(
|
|
@@ -20,6 +23,9 @@ exports._getPathOptions = function _getPathOptions(
|
|
|
20
23
|
originY,
|
|
21
24
|
) {
|
|
22
25
|
this.current = this.current || {};
|
|
26
|
+
// Validate before anything is resolved or drawn.
|
|
27
|
+
const rotation = rotationOption(options.rotation);
|
|
28
|
+
const miterLimit = miterLimitOption(options.miterLimit);
|
|
23
29
|
const colorspace = options.colorspace || this.options.colorspace;
|
|
24
30
|
const colorName = options.colorName;
|
|
25
31
|
|
|
@@ -94,18 +100,16 @@ exports._getPathOptions = function _getPathOptions(
|
|
|
94
100
|
|
|
95
101
|
// A fill or stroke replaces the default color. Resolving that default under
|
|
96
102
|
// colorName would write an unused Separation color space for the ink.
|
|
97
|
-
const paintsColor =
|
|
98
|
-
options.color || options.colour || (!options.fill && !options.stroke);
|
|
103
|
+
const paintsColor = options.color || (!options.fill && !options.stroke);
|
|
99
104
|
pathOptions.colorModel = this._transformColor(
|
|
100
|
-
options.color
|
|
105
|
+
options.color,
|
|
101
106
|
paintsColor ? colorOpts : { ...colorOpts, colorName: undefined },
|
|
102
107
|
);
|
|
103
108
|
pathOptions.color = pathOptions.colorModel.color;
|
|
104
109
|
pathOptions.colorspace = pathOptions.colorModel.colorspace;
|
|
105
110
|
|
|
106
111
|
// rotation
|
|
107
|
-
if (
|
|
108
|
-
const rotation = parseFloat(options.rotation);
|
|
112
|
+
if (rotation !== undefined) {
|
|
109
113
|
pathOptions.rotation = rotation;
|
|
110
114
|
pathOptions.rotationOrigin = options.rotationOrigin || null;
|
|
111
115
|
}
|
|
@@ -135,11 +139,7 @@ exports._getPathOptions = function _getPathOptions(
|
|
|
135
139
|
pathOptions.lineCap = this._lineCap(options.lineCap);
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
if (
|
|
139
|
-
if (!isNaN(options.miterLimit)) {
|
|
140
|
-
pathOptions.miterLimit = options.miterLimit;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
142
|
+
if (miterLimit !== undefined) pathOptions.miterLimit = miterLimit;
|
|
143
143
|
|
|
144
144
|
return pathOptions;
|
|
145
145
|
};
|
package/lib/recipe/vector.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const { linkPdf } = require("./annotation");
|
|
16
16
|
const muhammara = require("../muhammara");
|
|
17
|
+
const { miterLimitOption } = require("../recipe-options");
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Draw a circle
|
|
@@ -668,15 +669,19 @@ exports.pie = function pie(x, y, radius, startAngle, endAngle, options = {}) {
|
|
|
668
669
|
* @param {number} [options.lineWidth] - Alias for width.
|
|
669
670
|
* @param {number} [options.cap] - The PDF line cap style, a `LineCapStyle` value.
|
|
670
671
|
* @param {number} [options.join] - The PDF line join style: 0 miter, 1 round, 2 bevel.
|
|
671
|
-
* @param {number} [options.miterLimit] - The miter limit.
|
|
672
|
+
* @param {number} [options.miterLimit] - The miter limit, at least 1.
|
|
672
673
|
* @param {number[]} [options.dash] - The dash pattern.
|
|
673
674
|
* @param {number} [options.dashPhase] - The dash pattern phase.
|
|
674
675
|
* @returns {Recipe} The recipe instance.
|
|
675
676
|
* @throws {TypeError} If no page is active.
|
|
677
|
+
* @throws {RangeError} If `miterLimit` is not a number of at least 1.
|
|
676
678
|
*/
|
|
677
679
|
exports.lineStyle = function lineStyle(options = {}) {
|
|
678
680
|
// null options act like omitted options.
|
|
679
681
|
if (options === null) options = {};
|
|
682
|
+
// Validate before anything is written.
|
|
683
|
+
const miterLimit = miterLimitOption(options.miterLimit);
|
|
684
|
+
if (miterLimit !== undefined) options = { ...options, miterLimit };
|
|
680
685
|
this.current = this.current || {};
|
|
681
686
|
this.current.lineStyle = this.current.lineStyle || {};
|
|
682
687
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Validation of Recipe drawing options shared by shapes, text, and images.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a `rotation` option in degrees. A numeric string counts as its number.
|
|
5
|
+
* @param {*} value - The option value.
|
|
6
|
+
* @returns {number|undefined} The rotation, or undefined when omitted or null.
|
|
7
|
+
* @throws {TypeError} If the value is not a finite number: it used to write
|
|
8
|
+
* NaN transformation matrices.
|
|
9
|
+
*/
|
|
10
|
+
function rotationOption(value) {
|
|
11
|
+
if (value === undefined || value === null) return undefined;
|
|
12
|
+
const rotation = optionNumber(value);
|
|
13
|
+
if (!Number.isFinite(rotation)) {
|
|
14
|
+
throw new TypeError("rotation must be a finite number");
|
|
15
|
+
}
|
|
16
|
+
return rotation;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read a `miterLimit` option. A numeric string counts as its number.
|
|
21
|
+
* @param {*} value - The option value.
|
|
22
|
+
* @returns {number|undefined} The miter limit, or undefined when omitted or
|
|
23
|
+
* null.
|
|
24
|
+
* @throws {RangeError} If the value is not a number of at least 1, which PDF
|
|
25
|
+
* requires.
|
|
26
|
+
*/
|
|
27
|
+
function miterLimitOption(value) {
|
|
28
|
+
if (value === undefined || value === null) return undefined;
|
|
29
|
+
const miterLimit = optionNumber(value);
|
|
30
|
+
if (!(miterLimit >= 1) || !Number.isFinite(miterLimit)) {
|
|
31
|
+
throw new RangeError("miterLimit must be a number of at least 1");
|
|
32
|
+
}
|
|
33
|
+
return miterLimit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The number of a numeric option: a number, or a string holding one.
|
|
38
|
+
* @param {*} value - The option value.
|
|
39
|
+
* @returns {number} The number; NaN for any other value.
|
|
40
|
+
*/
|
|
41
|
+
function optionNumber(value) {
|
|
42
|
+
if (typeof value === "number") return value;
|
|
43
|
+
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
|
44
|
+
return NaN;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { miterLimitOption, rotationOption };
|
package/muhammara.d.ts
CHANGED
|
@@ -3332,20 +3332,30 @@ declare namespace muhammara {
|
|
|
3332
3332
|
userProtectionFlag?: number;
|
|
3333
3333
|
}
|
|
3334
3334
|
|
|
3335
|
-
|
|
3335
|
+
/**
|
|
3336
|
+
* Options of `image()`. `fill`, `stroke`, or `color` frame the drawn image
|
|
3337
|
+
* box: the fill beneath the image, the outline above it and inside the
|
|
3338
|
+
* box, as a `rectangle()` stroke. The line options style that outline.
|
|
3339
|
+
*/
|
|
3340
|
+
interface ImageOptions extends PathOptions, TransformOptions {
|
|
3336
3341
|
/** Make the rendered image open this URL. */
|
|
3337
3342
|
link?: string;
|
|
3343
|
+
/** The drawn image width. `lineWidth` sets the frame outline width. */
|
|
3338
3344
|
width?: number;
|
|
3339
3345
|
height?: number;
|
|
3346
|
+
/** Scale the source size; wins over `width` and `height`. */
|
|
3340
3347
|
scale?: number;
|
|
3341
3348
|
keepAspectRatio?: boolean;
|
|
3349
|
+
/** The opacity of the image and its frame. */
|
|
3342
3350
|
opacity?: number;
|
|
3343
3351
|
/** `Recipe.HorizontalAlign`, optionally followed by a space and `Recipe.VerticalAlign`. */
|
|
3344
3352
|
align?: ImageAlign;
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3353
|
+
/** The one-based page of a PDF source, or image of a multi-image TIFF, as in `overlay()`; defaults to 1. */
|
|
3354
|
+
page?: number;
|
|
3355
|
+
/** Paint the image box beneath the image. */
|
|
3356
|
+
fill?: Color;
|
|
3357
|
+
/** Outline the image box in green and mark the placement point in red. */
|
|
3358
|
+
debug?: boolean | number;
|
|
3349
3359
|
}
|
|
3350
3360
|
|
|
3351
3361
|
interface InfoOptions {
|
|
@@ -4151,19 +4161,36 @@ declare namespace muhammara {
|
|
|
4151
4161
|
* @param y - The coordinate y of the top-left corner
|
|
4152
4162
|
* @param options - The options
|
|
4153
4163
|
* @returns The recipe instance.
|
|
4154
|
-
* @param options.
|
|
4164
|
+
* @param options.page - The one-based page of a PDF source, or image of a
|
|
4165
|
+
* multi-image TIFF, as in `overlay()`; defaults to 1.
|
|
4166
|
+
* @param options.width - The new width. The frame outline width is
|
|
4167
|
+
* `options.lineWidth`.
|
|
4155
4168
|
* @param options.height - The new height
|
|
4156
4169
|
* @param options.scale - Scale the image from the original width and height.
|
|
4157
4170
|
* @param options.keepAspectRatio - Keep the aspect ratio; defaults to true.
|
|
4158
|
-
* @param options.opacity - The opacity.
|
|
4171
|
+
* @param options.opacity - The opacity of the image and its frame.
|
|
4159
4172
|
* @param options.align - A `Recipe.HorizontalAlign` value, optionally
|
|
4160
4173
|
* followed by a space and a `Recipe.VerticalAlign` value, for example
|
|
4161
4174
|
* "center center". Horizontal center moves the image left by half its width
|
|
4162
4175
|
* and right moves it right by half; vertical center moves it up by half its
|
|
4163
4176
|
* height and bottom moves it down by half from its top-left placement.
|
|
4177
|
+
* @param options.rotation - Rotate the image, in degrees.
|
|
4178
|
+
* @param options.rotationOrigin - [x, y] of the rotation origin; the
|
|
4179
|
+
* bottom-left corner of the drawn image when omitted.
|
|
4180
|
+
* @param options.fill - Paint the image box beneath the image.
|
|
4181
|
+
* @param options.stroke - Outline the image box above the image.
|
|
4182
|
+
* @param options.color - Outline color when `stroke` is not given.
|
|
4183
|
+
* @param options.lineWidth - The outline width; the outline lies inside
|
|
4184
|
+
* the image box, as a `rectangle()` stroke does.
|
|
4185
|
+
* @param options.debug - Outline the image box in green and mark the
|
|
4186
|
+
* placement point in red.
|
|
4164
4187
|
* @param options.link - Make the image open this URL.
|
|
4165
4188
|
* @throws {TypeError} If no page is active.
|
|
4166
|
-
* @throws {
|
|
4189
|
+
* @throws {RangeError} If `page` is not an integer from 1 to 4294967296,
|
|
4190
|
+
* a size is not a finite number, or `miterLimit` is not a number of at
|
|
4191
|
+
* least 1.
|
|
4192
|
+
* @throws {TypeError} If `rotation` is not a finite number.
|
|
4193
|
+
* @throws {Error} If the image, or the PDF page `page` selects, cannot be read.
|
|
4167
4194
|
*/
|
|
4168
4195
|
image(
|
|
4169
4196
|
imgSrc: string,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhammara/native-core",
|
|
3
|
-
"version": "7.0.0
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Shared JavaScript API for MuhammaraJS native packages",
|
|
5
5
|
"homepage": "https://muhammarajs.readthedocs.io/",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@xmldom/xmldom": "^0.9.10",
|
|
34
|
-
"linebreak": "^1.1.0"
|
|
35
|
-
"memory-streams": "^0.1.3"
|
|
34
|
+
"linebreak": "^1.1.0"
|
|
36
35
|
},
|
|
37
36
|
"publishConfig": {
|
|
38
37
|
"access": "public",
|