docxmlater 5.0.1 → 5.3.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.
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Image = void 0;
4
4
  const fs_1 = require("fs");
5
- const XMLBuilder_1 = require("../xml/XMLBuilder");
5
+ const logger_1 = require("../utils/logger");
6
6
  const units_1 = require("../utils/units");
7
+ const XMLBuilder_1 = require("../xml/XMLBuilder");
7
8
  class Image {
8
9
  source;
9
10
  width;
@@ -14,12 +15,36 @@ class Image {
14
15
  imageData;
15
16
  extension;
16
17
  docPrId = 1;
18
+ dpi = 96;
17
19
  effectExtent;
18
20
  wrap;
19
21
  position;
20
22
  anchor;
21
23
  crop;
22
24
  effects;
25
+ rotation = 0;
26
+ border;
27
+ static async fromFile(path, properties = {}) {
28
+ const image = new Image({ source: path, ...properties });
29
+ await image.loadImageDataForDimensions();
30
+ return image;
31
+ }
32
+ static async fromBuffer(buffer, properties = {}) {
33
+ const image = new Image({ source: buffer, ...properties });
34
+ await image.loadImageDataForDimensions();
35
+ return image;
36
+ }
37
+ static async create(properties) {
38
+ if (Buffer.isBuffer(properties.source)) {
39
+ return Image.fromBuffer(properties.source, properties);
40
+ }
41
+ else if (typeof properties.source === 'string') {
42
+ return Image.fromFile(properties.source, properties);
43
+ }
44
+ else {
45
+ throw new Error('Invalid source: must be file path or Buffer');
46
+ }
47
+ }
23
48
  constructor(properties) {
24
49
  this.source = properties.source;
25
50
  this.description = properties.description || 'Image';
@@ -34,55 +59,86 @@ class Image {
34
59
  this.anchor = properties.anchor;
35
60
  this.crop = properties.crop;
36
61
  this.effects = properties.effects;
62
+ this.dpi = 96;
37
63
  }
38
64
  async loadImageDataForDimensions() {
39
65
  let tempData;
40
- if (Buffer.isBuffer(this.source)) {
41
- tempData = this.source;
42
- }
43
- else if (typeof this.source === 'string') {
44
- try {
66
+ try {
67
+ if (Buffer.isBuffer(this.source)) {
68
+ tempData = this.source;
69
+ }
70
+ else if (typeof this.source === 'string') {
45
71
  await fs_1.promises.access(this.source);
46
72
  tempData = await fs_1.promises.readFile(this.source);
47
73
  }
48
- catch (error) {
49
- throw new Error(`Could not read image file: ${this.source}`);
74
+ if (tempData) {
75
+ this.imageData = tempData;
76
+ const dimensions = this.detectDimensions();
77
+ if (dimensions) {
78
+ this.dpi = this.detectDPI() || 96;
79
+ const emuPerInch = 914400;
80
+ const pixelsPerInch = this.dpi;
81
+ this.width = Math.round((dimensions.width / pixelsPerInch) * emuPerInch);
82
+ this.height = Math.round((dimensions.height / pixelsPerInch) * emuPerInch);
83
+ }
84
+ if (typeof this.source === 'string') {
85
+ this.imageData = undefined;
86
+ }
50
87
  }
51
88
  }
52
- if (tempData) {
53
- this.imageData = tempData;
54
- const dimensions = this.detectDimensions();
55
- if (dimensions) {
56
- this.width = dimensions.width;
57
- this.height = dimensions.height;
58
- }
59
- if (typeof this.source === 'string') {
60
- this.imageData = undefined;
61
- }
89
+ catch (error) {
90
+ logger_1.defaultLogger.error(`Failed to load image for dimensions: ${error instanceof Error ? error.message : String(error)}`);
91
+ throw new Error(`Image loading failed: ${error instanceof Error ? error.message : String(error)}`);
62
92
  }
63
93
  }
64
94
  async ensureDataLoaded() {
65
- if (this.imageData) {
95
+ if (this.imageData)
66
96
  return;
67
- }
68
- if (Buffer.isBuffer(this.source)) {
69
- this.imageData = this.source;
70
- }
71
- else if (typeof this.source === 'string') {
72
- try {
97
+ try {
98
+ if (Buffer.isBuffer(this.source)) {
99
+ this.imageData = this.source;
100
+ }
101
+ else if (typeof this.source === 'string') {
73
102
  await fs_1.promises.access(this.source);
74
103
  this.imageData = await fs_1.promises.readFile(this.source);
75
104
  }
76
- catch (error) {
77
- throw new Error(`Failed to load image from ${this.source}: ${error instanceof Error ? error.message : error}`);
105
+ else {
106
+ throw new Error('Invalid image source');
78
107
  }
79
108
  }
109
+ catch (error) {
110
+ logger_1.defaultLogger.error(`Failed to load image data: ${error instanceof Error ? error.message : String(error)}`);
111
+ throw new Error(`Image data loading failed: ${error instanceof Error ? error.message : String(error)}`);
112
+ }
80
113
  }
81
114
  releaseData() {
82
115
  if (typeof this.source === 'string') {
83
116
  this.imageData = undefined;
84
117
  }
85
118
  }
119
+ validateImageData() {
120
+ if (!this.imageData || this.imageData.length === 0) {
121
+ return { valid: false, error: 'Empty image data' };
122
+ }
123
+ const signatures = {
124
+ png: [0x89, 0x50, 0x4E, 0x47],
125
+ jpg: [0xFF, 0xD8],
126
+ jpeg: [0xFF, 0xD8],
127
+ gif: [0x47, 0x49, 0x46],
128
+ bmp: [0x42, 0x4D],
129
+ tiff: [0x49, 0x49, 0x2A, 0x00],
130
+ tif: [0x49, 0x49, 0x2A, 0x00]
131
+ };
132
+ const sig = signatures[this.extension];
133
+ if (sig) {
134
+ for (let i = 0; i < sig.length; i++) {
135
+ if (this.imageData[i] !== sig[i]) {
136
+ return { valid: false, error: `Invalid ${this.extension.toUpperCase()} signature` };
137
+ }
138
+ }
139
+ }
140
+ return { valid: true };
141
+ }
86
142
  detectExtension() {
87
143
  if (typeof this.source === 'string') {
88
144
  const match = this.source.match(/\.([a-z]+)$/i);
@@ -93,225 +149,138 @@ class Image {
93
149
  return 'png';
94
150
  }
95
151
  detectDimensions() {
96
- if (!this.imageData || this.imageData.length < 24) {
152
+ if (!this.imageData || this.imageData.length < 24)
97
153
  return null;
154
+ if (this.imageData[0] === 0x89 && this.imageData[1] === 0x50 && this.imageData[2] === 0x4e && this.imageData[3] === 0x47) {
155
+ return this.detectPngDimensions();
98
156
  }
99
- try {
100
- if (this.imageData[0] === 0x89 &&
101
- this.imageData[1] === 0x50 &&
102
- this.imageData[2] === 0x4e &&
103
- this.imageData[3] === 0x47) {
104
- return this.detectPngDimensions();
105
- }
106
- if (this.imageData[0] === 0xff && this.imageData[1] === 0xd8) {
107
- return this.detectJpegDimensions();
108
- }
109
- if (this.imageData[0] === 0x47 &&
110
- this.imageData[1] === 0x49 &&
111
- this.imageData[2] === 0x46) {
112
- return this.detectGifDimensions();
113
- }
114
- if (this.imageData[0] === 0x42 && this.imageData[1] === 0x4d) {
115
- return this.detectBmpDimensions();
116
- }
117
- if ((this.imageData[0] === 0x49 && this.imageData[1] === 0x49 && this.imageData[2] === 0x2a) ||
118
- (this.imageData[0] === 0x4d && this.imageData[1] === 0x4d && this.imageData[2] === 0x00)) {
119
- return this.detectTiffDimensions();
120
- }
157
+ if (this.imageData[0] === 0xff && this.imageData[1] === 0xd8) {
158
+ return this.detectJpegDimensions();
121
159
  }
122
- catch (error) {
160
+ if (this.imageData[0] === 0x47 && this.imageData[1] === 0x49 && this.imageData[2] === 0x46) {
161
+ return this.detectGifDimensions();
162
+ }
163
+ if (this.imageData[0] === 0x42 && this.imageData[1] === 0x4d) {
164
+ return this.detectBmpDimensions();
165
+ }
166
+ if ((this.imageData[0] === 0x49 && this.imageData[1] === 0x49 && this.imageData[2] === 0x2a) ||
167
+ (this.imageData[0] === 0x4d && this.imageData[1] === 0x4d && this.imageData[2] === 0x00)) {
168
+ return this.detectTiffDimensions();
123
169
  }
124
170
  return null;
125
171
  }
126
172
  detectPngDimensions() {
127
- if (!this.imageData || this.imageData.length < 24) {
128
- return null;
129
- }
130
- try {
131
- const width = this.imageData.readUInt32BE(16);
132
- const height = this.imageData.readUInt32BE(20);
133
- return {
134
- width: Math.round((width / 96) * 914400),
135
- height: Math.round((height / 96) * 914400),
136
- };
137
- }
138
- catch (error) {
173
+ if (!this.imageData || this.imageData.length < 24)
139
174
  return null;
140
- }
175
+ const width = this.imageData.readUInt32BE(16);
176
+ const height = this.imageData.readUInt32BE(20);
177
+ return { width, height };
141
178
  }
142
179
  detectGifDimensions() {
143
- if (!this.imageData || this.imageData.length < 10) {
180
+ if (!this.imageData || this.imageData.length < 10)
144
181
  return null;
145
- }
146
- try {
147
- const width = this.imageData.readUInt16LE(6);
148
- const height = this.imageData.readUInt16LE(8);
149
- if (width > 0 && height > 0 && width < 65535 && height < 65535) {
150
- return {
151
- width: Math.round((width / 96) * 914400),
152
- height: Math.round((height / 96) * 914400),
153
- };
154
- }
155
- }
156
- catch (error) {
157
- }
182
+ const width = this.imageData.readUInt16LE(6);
183
+ const height = this.imageData.readUInt16LE(8);
184
+ if (width > 0 && height > 0)
185
+ return { width, height };
158
186
  return null;
159
187
  }
160
188
  detectBmpDimensions() {
161
- if (!this.imageData || this.imageData.length < 26) {
189
+ if (!this.imageData || this.imageData.length < 26)
162
190
  return null;
163
- }
164
- try {
165
- const width = this.imageData.readInt32LE(18);
166
- const height = Math.abs(this.imageData.readInt32LE(22));
167
- if (width > 0 && height > 0 && width < 65535 && height < 65535) {
168
- return {
169
- width: Math.round((width / 96) * 914400),
170
- height: Math.round((height / 96) * 914400),
171
- };
172
- }
173
- }
174
- catch (error) {
175
- }
191
+ const width = this.imageData.readInt32LE(18);
192
+ const height = Math.abs(this.imageData.readInt32LE(22));
193
+ if (width > 0 && height > 0)
194
+ return { width, height };
176
195
  return null;
177
196
  }
178
197
  detectTiffDimensions() {
179
- if (!this.imageData || this.imageData.length < 14) {
198
+ if (!this.imageData || this.imageData.length < 14)
180
199
  return null;
181
- }
182
- try {
183
- const isLittleEndian = this.imageData[0] === 0x49;
184
- const ifdOffset = isLittleEndian
185
- ? this.imageData.readUInt32LE(4)
186
- : this.imageData.readUInt32BE(4);
187
- if (ifdOffset + 14 > this.imageData.length) {
188
- return null;
189
- }
190
- const numEntries = isLittleEndian
191
- ? this.imageData.readUInt16LE(ifdOffset)
192
- : this.imageData.readUInt16BE(ifdOffset);
193
- let width = 0;
194
- let height = 0;
195
- for (let i = 0; i < numEntries; i++) {
196
- const entryOffset = ifdOffset + 2 + i * 12;
197
- if (entryOffset + 12 > this.imageData.length) {
198
- break;
199
- }
200
- const tag = isLittleEndian
201
- ? this.imageData.readUInt16LE(entryOffset)
202
- : this.imageData.readUInt16BE(entryOffset);
203
- const value = isLittleEndian
204
- ? this.imageData.readUInt32LE(entryOffset + 8)
205
- : this.imageData.readUInt32BE(entryOffset + 8);
206
- if (tag === 256 || tag === 0x100) {
207
- width = value;
208
- }
209
- else if (tag === 257 || tag === 0x101) {
210
- height = value;
211
- }
212
- if (width > 0 && height > 0) {
213
- break;
214
- }
215
- }
216
- if (width > 0 && height > 0 && width < 65535 && height < 65535) {
217
- return {
218
- width: Math.round((width / 96) * 914400),
219
- height: Math.round((height / 96) * 914400),
220
- };
221
- }
222
- }
223
- catch (error) {
224
- }
200
+ const isLittleEndian = this.imageData[0] === 0x49;
201
+ const ifdOffset = isLittleEndian ? this.imageData.readUInt32LE(4) : this.imageData.readUInt32BE(4);
202
+ if (ifdOffset + 14 > this.imageData.length)
203
+ return null;
204
+ const numEntries = isLittleEndian ? this.imageData.readUInt16LE(ifdOffset) : this.imageData.readUInt16BE(ifdOffset);
205
+ let width = 0;
206
+ let height = 0;
207
+ for (let i = 0; i < numEntries; i++) {
208
+ const entryOffset = ifdOffset + 2 + i * 12;
209
+ if (entryOffset + 12 > this.imageData.length)
210
+ break;
211
+ const tag = isLittleEndian ? this.imageData.readUInt16LE(entryOffset) : this.imageData.readUInt16BE(entryOffset);
212
+ const value = isLittleEndian ? this.imageData.readUInt32LE(entryOffset + 8) : this.imageData.readUInt32BE(entryOffset + 8);
213
+ if (tag === 256)
214
+ width = value;
215
+ if (tag === 257)
216
+ height = value;
217
+ if (width > 0 && height > 0)
218
+ break;
219
+ }
220
+ if (width > 0 && height > 0)
221
+ return { width, height };
225
222
  return null;
226
223
  }
227
224
  detectJpegDimensions() {
228
- if (!this.imageData || this.imageData.length < 12) {
225
+ if (!this.imageData || this.imageData.length < 12)
229
226
  return null;
230
- }
231
- try {
232
- if (this.imageData[0] !== 0xff || this.imageData[1] !== 0xd8) {
233
- return null;
234
- }
235
- let offset = 2;
236
- while (offset < this.imageData.length - 1) {
237
- if (this.imageData[offset] !== 0xff) {
238
- break;
239
- }
240
- const marker = this.imageData[offset + 1];
241
- if (marker === undefined) {
242
- break;
243
- }
244
- if (marker === 0x00 || marker === 0xff) {
245
- offset++;
246
- continue;
247
- }
248
- const isSOF = (marker >= 0xc0 && marker <= 0xc3) ||
249
- (marker >= 0xc5 && marker <= 0xc7) ||
250
- (marker >= 0xc9 && marker <= 0xcb) ||
251
- (marker >= 0xcd && marker <= 0xcf);
252
- if (isSOF) {
253
- if (offset + 9 > this.imageData.length) {
254
- break;
255
- }
256
- const height = this.imageData.readUInt16BE(offset + 5);
257
- const width = this.imageData.readUInt16BE(offset + 7);
258
- if (width > 0 && height > 0 && width < 65535 && height < 65535) {
259
- return {
260
- width: Math.round((width / 96) * 914400),
261
- height: Math.round((height / 96) * 914400),
262
- };
263
- }
264
- }
265
- if (marker === 0xda) {
266
- break;
267
- }
268
- if (marker === 0xd9) {
269
- break;
270
- }
271
- const standaloneMarker = (marker >= 0xd0 && marker <= 0xd9) || marker === 0x01;
272
- if (standaloneMarker) {
273
- offset += 2;
274
- continue;
275
- }
276
- if (offset + 3 > this.imageData.length) {
277
- break;
278
- }
279
- const segmentLength = this.imageData.readUInt16BE(offset + 2);
280
- if (segmentLength < 2 || offset + 2 + segmentLength > this.imageData.length) {
227
+ let offset = 2;
228
+ while (offset < this.imageData.length - 1) {
229
+ if (this.imageData[offset] !== 0xff)
230
+ break;
231
+ const marker = this.imageData[offset + 1];
232
+ if (marker === undefined)
233
+ break;
234
+ if (marker === 0x00 || marker === 0xff) {
235
+ offset++;
236
+ continue;
237
+ }
238
+ const isSOF = (marker >= 0xc0 && marker <= 0xcf) && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
239
+ if (isSOF) {
240
+ if (offset + 9 > this.imageData.length)
281
241
  break;
282
- }
283
- offset += 2 + segmentLength;
284
- }
285
- }
286
- catch (error) {
242
+ const height = this.imageData.readUInt16BE(offset + 5);
243
+ const width = this.imageData.readUInt16BE(offset + 7);
244
+ if (width > 0 && height > 0)
245
+ return { width, height };
246
+ }
247
+ if (marker === 0xda || marker === 0xd9)
248
+ break;
249
+ const segmentLength = this.imageData.readUInt16BE(offset + 2);
250
+ if (segmentLength < 2 || offset + 2 + segmentLength > this.imageData.length)
251
+ break;
252
+ offset += 2 + segmentLength;
287
253
  }
288
254
  return null;
289
255
  }
290
256
  async getImageDataAsync() {
291
257
  await this.ensureDataLoaded();
292
- if (this.imageData) {
293
- return this.imageData;
294
- }
295
- throw new Error('Failed to load image data');
258
+ if (!this.imageData)
259
+ throw new Error('Failed to load image data');
260
+ return this.imageData;
296
261
  }
297
262
  getImageData() {
298
- if (!this.imageData) {
299
- throw new Error('Image data not loaded. ' +
300
- 'Call await image.ensureDataLoaded() or await imageManager.loadAllImageData() first.');
301
- }
263
+ if (!this.imageData)
264
+ throw new Error('Image data not loaded. Call ensureDataLoaded first.');
302
265
  return this.imageData;
303
266
  }
304
267
  getExtension() {
305
268
  return this.extension;
306
269
  }
270
+ getDPI() {
271
+ return this.dpi;
272
+ }
307
273
  getWidth() {
308
274
  return this.width;
309
275
  }
310
276
  getHeight() {
311
277
  return this.height;
312
278
  }
279
+ getImageDataSafe() {
280
+ return this.imageData ?? null;
281
+ }
313
282
  setWidth(width, maintainAspectRatio = true) {
314
- if (maintainAspectRatio) {
283
+ if (maintainAspectRatio && this.height > 0) {
315
284
  const ratio = this.height / this.width;
316
285
  this.height = Math.round(width * ratio);
317
286
  }
@@ -319,7 +288,7 @@ class Image {
319
288
  return this;
320
289
  }
321
290
  setHeight(height, maintainAspectRatio = true) {
322
- if (maintainAspectRatio) {
291
+ if (maintainAspectRatio && this.width > 0) {
323
292
  const ratio = this.width / this.height;
324
293
  this.width = Math.round(height * ratio);
325
294
  }
@@ -331,6 +300,14 @@ class Image {
331
300
  this.height = height;
332
301
  return this;
333
302
  }
303
+ async updateImageData(newSource) {
304
+ this.source = newSource;
305
+ this.imageData = undefined;
306
+ await this.loadImageDataForDimensions();
307
+ if (typeof newSource === 'string')
308
+ this.extension = this.detectExtension();
309
+ this.dpi = this.detectDPI() || 96;
310
+ }
334
311
  setRelationshipId(relationshipId) {
335
312
  this.relationshipId = relationshipId;
336
313
  return this;
@@ -350,17 +327,14 @@ class Image {
350
327
  return this.description;
351
328
  }
352
329
  rotate(degrees) {
353
- const normalizedDegrees = ((degrees % 360) + 360) % 360;
354
- this.rotation = normalizedDegrees;
355
- if (normalizedDegrees === 90 || normalizedDegrees === 270) {
356
- const temp = this.width;
357
- this.width = this.height;
358
- this.height = temp;
330
+ this.rotation = ((degrees % 360) + 360) % 360;
331
+ if (this.rotation === 90 || this.rotation === 270) {
332
+ [this.width, this.height] = [this.height, this.width];
359
333
  }
360
334
  return this;
361
335
  }
362
336
  getRotation() {
363
- return this.rotation || 0;
337
+ return this.rotation;
364
338
  }
365
339
  setEffectExtent(left, top, right, bottom) {
366
340
  this.effectExtent = { left, top, right, bottom };
@@ -370,14 +344,7 @@ class Image {
370
344
  return this.effectExtent;
371
345
  }
372
346
  setWrap(type, side, distances) {
373
- this.wrap = {
374
- type,
375
- side,
376
- distanceTop: distances?.top,
377
- distanceBottom: distances?.bottom,
378
- distanceLeft: distances?.left,
379
- distanceRight: distances?.right,
380
- };
347
+ this.wrap = { type, side, ...distances };
381
348
  return this;
382
349
  }
383
350
  getWrap() {
@@ -399,12 +366,7 @@ class Image {
399
366
  }
400
367
  setCrop(left, top, right, bottom) {
401
368
  const clamp = (val) => Math.max(0, Math.min(100, val));
402
- this.crop = {
403
- left: clamp(left),
404
- top: clamp(top),
405
- right: clamp(right),
406
- bottom: clamp(bottom),
407
- };
369
+ this.crop = { left: clamp(left), top: clamp(top), right: clamp(right), bottom: clamp(bottom) };
408
370
  return this;
409
371
  }
410
372
  getCrop() {
@@ -412,440 +374,219 @@ class Image {
412
374
  }
413
375
  setEffects(options) {
414
376
  const clamp = (val) => val !== undefined ? Math.max(-100, Math.min(100, val)) : undefined;
415
- this.effects = {
416
- brightness: clamp(options.brightness),
417
- contrast: clamp(options.contrast),
418
- grayscale: options.grayscale,
419
- };
377
+ this.effects = { brightness: clamp(options.brightness), contrast: clamp(options.contrast), grayscale: options.grayscale };
420
378
  return this;
421
379
  }
422
380
  getEffects() {
423
381
  return this.effects;
424
382
  }
425
- isFloating() {
426
- return this.anchor !== undefined || this.position !== undefined;
427
- }
428
- toXML() {
429
- if (!this.relationshipId) {
430
- throw new Error('Image must have a relationship ID before generating XML');
431
- }
432
- const imageElement = this.isFloating() ? this.createAnchor() : this.createInline();
433
- const drawing = XMLBuilder_1.XMLBuilder.w('drawing', undefined, [imageElement]);
434
- return drawing;
435
- }
436
- createInline() {
437
- const children = [];
438
- children.push({
439
- name: 'wp:extent',
440
- attributes: {
441
- cx: this.width.toString(),
442
- cy: this.height.toString(),
443
- },
444
- selfClosing: true,
445
- });
446
- const effectExt = this.effectExtent || { left: 0, top: 0, right: 0, bottom: 0 };
447
- children.push({
448
- name: 'wp:effectExtent',
449
- attributes: {
450
- l: effectExt.left.toString(),
451
- t: effectExt.top.toString(),
452
- r: effectExt.right.toString(),
453
- b: effectExt.bottom.toString(),
454
- },
455
- selfClosing: true,
456
- });
457
- children.push({
458
- name: 'wp:docPr',
459
- attributes: {
460
- id: this.docPrId.toString(),
461
- name: this.name,
462
- descr: this.description,
463
- },
464
- selfClosing: true,
465
- });
466
- children.push({
467
- name: 'wp:cNvGraphicFramePr',
468
- children: [
469
- {
470
- name: 'a:graphicFrameLocks',
471
- attributes: {
472
- 'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
473
- noChangeAspect: '1',
474
- },
475
- selfClosing: true,
476
- },
477
- ],
478
- });
479
- children.push(this.createGraphic());
480
- return {
481
- name: 'wp:inline',
482
- attributes: {
483
- distT: '0',
484
- distB: '0',
485
- distL: '0',
486
- distR: '0',
487
- },
488
- children,
489
- };
490
- }
491
- createGraphic() {
492
- return {
493
- name: 'a:graphic',
494
- attributes: {
495
- 'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
496
- },
497
- children: [
498
- {
499
- name: 'a:graphicData',
500
- attributes: {
501
- uri: 'http://schemas.openxmlformats.org/drawingml/2006/picture',
502
- },
503
- children: [this.createPicture()],
504
- },
505
- ],
506
- };
507
- }
508
- createPicture() {
509
- return {
510
- name: 'pic:pic',
511
- attributes: {
512
- 'xmlns:pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture',
513
- },
514
- children: [
515
- {
516
- name: 'pic:nvPicPr',
517
- children: [
518
- {
519
- name: 'pic:cNvPr',
520
- attributes: {
521
- id: this.docPrId.toString(),
522
- name: this.name,
523
- descr: this.description,
524
- },
525
- selfClosing: true,
526
- },
527
- {
528
- name: 'pic:cNvPicPr',
529
- selfClosing: true,
530
- },
531
- ],
532
- },
533
- {
534
- name: 'pic:blipFill',
535
- children: this.createBlipFillChildren(),
536
- },
537
- {
538
- name: 'pic:spPr',
539
- children: [
540
- {
541
- name: 'a:xfrm',
542
- attributes: this.getRotation() > 0 ? { rot: (this.getRotation() * 60000).toString() } : undefined,
543
- children: [
544
- {
545
- name: 'a:off',
546
- attributes: {
547
- x: '0',
548
- y: '0',
549
- },
550
- selfClosing: true,
551
- },
552
- {
553
- name: 'a:ext',
554
- attributes: {
555
- cx: this.width.toString(),
556
- cy: this.height.toString(),
557
- },
558
- selfClosing: true,
559
- },
560
- ],
561
- },
562
- {
563
- name: 'a:prstGeom',
564
- attributes: {
565
- prst: 'rect',
566
- },
567
- children: [
568
- {
569
- name: 'a:avLst',
570
- selfClosing: true,
571
- },
572
- ],
573
- },
574
- ],
575
- },
576
- ],
577
- };
578
- }
579
- createBlipFillChildren() {
580
- const children = [];
581
- const blipChildren = [];
582
- if (this.crop) {
583
- blipChildren.push({
584
- name: 'a:srcRect',
585
- attributes: {
586
- l: Math.round(this.crop.left * 1000).toString(),
587
- t: Math.round(this.crop.top * 1000).toString(),
588
- r: Math.round(this.crop.right * 1000).toString(),
589
- b: Math.round(this.crop.bottom * 1000).toString(),
590
- },
591
- selfClosing: true,
592
- });
593
- }
594
- if (this.effects) {
595
- const lumAttrs = {};
596
- if (this.effects.brightness !== undefined) {
597
- lumAttrs.bright = Math.round(this.effects.brightness * 1000).toString();
598
- }
599
- if (this.effects.contrast !== undefined) {
600
- lumAttrs.contrast = Math.round(this.effects.contrast * 1000).toString();
601
- }
602
- if (Object.keys(lumAttrs).length > 0) {
603
- blipChildren.push({
604
- name: 'a:lum',
605
- attributes: lumAttrs,
606
- selfClosing: true,
607
- });
383
+ detectDPI() {
384
+ if (!this.imageData)
385
+ return undefined;
386
+ try {
387
+ if (this.extension === 'png') {
388
+ const physIndex = this.imageData.indexOf(Buffer.from([0x70, 0x48, 0x59, 0x73]));
389
+ if (physIndex !== -1 && physIndex + 12 < this.imageData.length) {
390
+ const xPixelsPerMeter = this.imageData.readUInt32BE(physIndex + 4);
391
+ const yPixelsPerMeter = this.imageData.readUInt32BE(physIndex + 8);
392
+ const unit = this.imageData[physIndex + 12];
393
+ if (unit === 1) {
394
+ const dpiX = Math.round(xPixelsPerMeter * 0.0254);
395
+ const dpiY = Math.round(yPixelsPerMeter * 0.0254);
396
+ return Math.min(dpiX, dpiY);
397
+ }
398
+ }
608
399
  }
609
- if (this.effects.grayscale) {
610
- blipChildren.push({
611
- name: 'a:grayscl',
612
- selfClosing: true,
613
- });
400
+ else if (this.extension === 'jpg' || this.extension === 'jpeg') {
401
+ let offset = 2;
402
+ while (offset < this.imageData.length) {
403
+ if (this.imageData[offset] !== 0xFF)
404
+ break;
405
+ const marker = this.imageData[offset + 1];
406
+ if (marker === 0xE0) {
407
+ const length = this.imageData.readUInt16BE(offset + 2);
408
+ if (length >= 16 && this.imageData.slice(offset + 4, offset + 9).toString('ascii') === 'JFIF\0') {
409
+ const units = this.imageData[offset + 11];
410
+ const xDensity = this.imageData.readUInt16BE(offset + 12);
411
+ const yDensity = this.imageData.readUInt16BE(offset + 14);
412
+ if (units === 1)
413
+ return Math.min(xDensity, yDensity);
414
+ if (units === 2)
415
+ return Math.min(Math.round(xDensity * 2.54), Math.round(yDensity * 2.54));
416
+ }
417
+ offset += 2 + length;
418
+ continue;
419
+ }
420
+ offset += 2 + this.imageData.readUInt16BE(offset + 2);
421
+ }
614
422
  }
615
423
  }
616
- children.push({
617
- name: 'a:blip',
618
- attributes: {
619
- 'r:embed': this.relationshipId,
620
- },
621
- ...(blipChildren.length > 0 ? { children: blipChildren } : { selfClosing: true }),
622
- });
623
- children.push({
624
- name: 'a:stretch',
625
- children: [
626
- {
627
- name: 'a:fillRect',
628
- selfClosing: true,
629
- },
630
- ],
424
+ catch (error) {
425
+ logger_1.defaultLogger.warn(`DPI detection failed: ${error instanceof Error ? error.message : String(error)}`);
426
+ }
427
+ return undefined;
428
+ }
429
+ isFloating() {
430
+ return !!this.anchor || !!this.position;
431
+ }
432
+ floatTopLeft(marginTop = 0, marginLeft = 0) {
433
+ this.setPosition({ anchor: 'page', offset: marginLeft }, { anchor: 'page', offset: marginTop });
434
+ this.setAnchor({
435
+ behindDoc: false,
436
+ locked: false,
437
+ layoutInCell: true,
438
+ allowOverlap: true,
439
+ relativeHeight: 251658240
631
440
  });
632
- return children;
441
+ this.setWrap('square', 'bothSides');
442
+ return this;
633
443
  }
634
- createAnchor() {
635
- const children = [];
636
- const anchorConfig = this.anchor || {
444
+ floatTopRight(marginTop = 0, marginRight = 0) {
445
+ this.setPosition({ anchor: 'page', alignment: 'right', offset: -marginRight }, { anchor: 'page', offset: marginTop });
446
+ this.setAnchor({
637
447
  behindDoc: false,
638
448
  locked: false,
639
449
  layoutInCell: true,
640
- allowOverlap: false,
641
- relativeHeight: 251658240,
642
- };
643
- if (this.position) {
644
- const posH = this.position.horizontal;
645
- const posHChildren = [];
646
- if (posH.offset !== undefined) {
647
- posHChildren.push({
648
- name: 'wp:posOffset',
649
- children: [posH.offset.toString()],
650
- });
651
- }
652
- else if (posH.alignment) {
653
- posHChildren.push({
654
- name: 'wp:align',
655
- children: [posH.alignment],
656
- });
657
- }
658
- children.push({
659
- name: 'wp:positionH',
660
- attributes: {
661
- relativeFrom: posH.anchor,
662
- },
663
- children: posHChildren,
664
- });
665
- }
666
- if (this.position) {
667
- const posV = this.position.vertical;
668
- const posVChildren = [];
669
- if (posV.offset !== undefined) {
670
- posVChildren.push({
671
- name: 'wp:posOffset',
672
- children: [posV.offset.toString()],
673
- });
674
- }
675
- else if (posV.alignment) {
676
- posVChildren.push({
677
- name: 'wp:align',
678
- children: [posV.alignment],
679
- });
680
- }
681
- children.push({
682
- name: 'wp:positionV',
683
- attributes: {
684
- relativeFrom: posV.anchor,
685
- },
686
- children: posVChildren,
687
- });
688
- }
689
- children.push({
690
- name: 'wp:extent',
691
- attributes: {
692
- cx: this.width.toString(),
693
- cy: this.height.toString(),
694
- },
695
- selfClosing: true,
450
+ allowOverlap: true,
451
+ relativeHeight: 251658240
696
452
  });
697
- const effectExt = this.effectExtent || { left: 0, top: 0, right: 0, bottom: 0 };
698
- children.push({
699
- name: 'wp:effectExtent',
700
- attributes: {
701
- l: effectExt.left.toString(),
702
- t: effectExt.top.toString(),
703
- r: effectExt.right.toString(),
704
- b: effectExt.bottom.toString(),
705
- },
706
- selfClosing: true,
453
+ this.setWrap('square', 'bothSides');
454
+ return this;
455
+ }
456
+ floatCenter() {
457
+ this.setPosition({ anchor: 'page', alignment: 'center' }, { anchor: 'page', alignment: 'center' });
458
+ this.setAnchor({
459
+ behindDoc: false,
460
+ locked: false,
461
+ layoutInCell: true,
462
+ allowOverlap: true,
463
+ relativeHeight: 251658240
707
464
  });
708
- if (this.wrap) {
709
- children.push(this.createWrapElement());
465
+ this.setWrap('square', 'bothSides');
466
+ return this;
467
+ }
468
+ setBehindText(behind = true) {
469
+ if (this.anchor) {
470
+ this.anchor.behindDoc = behind;
710
471
  }
711
472
  else {
712
- children.push({
713
- name: 'wp:wrapSquare',
714
- attributes: {
715
- wrapText: 'bothSides',
716
- },
717
- selfClosing: true,
473
+ this.setAnchor({
474
+ behindDoc: behind,
475
+ locked: false,
476
+ layoutInCell: true,
477
+ allowOverlap: true,
478
+ relativeHeight: 251658240
718
479
  });
719
480
  }
720
- children.push({
721
- name: 'wp:docPr',
722
- attributes: {
723
- id: this.docPrId.toString(),
724
- name: this.name,
725
- descr: this.description,
726
- },
727
- selfClosing: true,
728
- });
729
- children.push({
730
- name: 'wp:cNvGraphicFramePr',
731
- children: [
732
- {
733
- name: 'a:graphicFrameLocks',
734
- attributes: {
735
- 'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
736
- noChangeAspect: '1',
737
- },
738
- selfClosing: true,
739
- },
740
- ],
741
- });
742
- children.push(this.createGraphic());
743
- return {
744
- name: 'wp:anchor',
745
- attributes: {
746
- distT: this.wrap?.distanceTop?.toString() || '0',
747
- distB: this.wrap?.distanceBottom?.toString() || '0',
748
- distL: this.wrap?.distanceLeft?.toString() || '0',
749
- distR: this.wrap?.distanceRight?.toString() || '0',
750
- simplePos: '0',
751
- relativeHeight: anchorConfig.relativeHeight.toString(),
752
- behindDoc: anchorConfig.behindDoc ? '1' : '0',
753
- locked: anchorConfig.locked ? '1' : '0',
754
- layoutInCell: anchorConfig.layoutInCell ? '1' : '0',
755
- allowOverlap: anchorConfig.allowOverlap ? '1' : '0',
756
- },
757
- children,
758
- };
759
- }
760
- createWrapElement() {
761
- if (!this.wrap) {
762
- return {
763
- name: 'wp:wrapSquare',
764
- attributes: {
765
- wrapText: 'bothSides',
766
- },
767
- selfClosing: true,
768
- };
769
- }
770
- const wrapAttributes = {
771
- wrapText: this.wrap.side || 'bothSides',
772
- };
773
- if (this.wrap.distanceTop !== undefined) {
774
- wrapAttributes.distT = this.wrap.distanceTop.toString();
775
- }
776
- if (this.wrap.distanceBottom !== undefined) {
777
- wrapAttributes.distB = this.wrap.distanceBottom.toString();
778
- }
779
- if (this.wrap.distanceLeft !== undefined) {
780
- wrapAttributes.distL = this.wrap.distanceLeft.toString();
781
- }
782
- if (this.wrap.distanceRight !== undefined) {
783
- wrapAttributes.distR = this.wrap.distanceRight.toString();
784
- }
785
- const wrapElementName = (() => {
786
- switch (this.wrap.type) {
787
- case 'square':
788
- return 'wp:wrapSquare';
789
- case 'tight':
790
- return 'wp:wrapTight';
791
- case 'through':
792
- return 'wp:wrapThrough';
793
- case 'topAndBottom':
794
- return 'wp:wrapTopAndBottom';
795
- case 'none':
796
- return 'wp:wrapNone';
797
- default:
798
- return 'wp:wrapSquare';
799
- }
800
- })();
801
- return {
802
- name: wrapElementName,
803
- attributes: wrapAttributes,
804
- selfClosing: true,
805
- };
806
- }
807
- static async fromFile(filePath, width, height) {
808
- const image = new Image({
809
- source: filePath,
810
- width,
811
- height,
812
- name: filePath.split(/[/\\]/).pop() || 'image',
813
- });
814
- if (!width || !height) {
815
- try {
816
- await image.loadImageDataForDimensions();
817
- }
818
- catch (error) {
819
- }
820
- }
821
- return image;
481
+ return this;
822
482
  }
823
- static async fromBuffer(buffer, extension, width, height) {
824
- const image = new Image({
825
- source: buffer,
826
- width,
827
- height,
828
- name: `image.${extension}`,
829
- });
830
- image.extension = extension;
831
- if (!width || !height) {
832
- await image.loadImageDataForDimensions();
833
- }
834
- return image;
483
+ applyTwoPixelBlackBorder() {
484
+ this.border = { width: 2, color: '000000' };
485
+ return this;
835
486
  }
836
- static async create(properties) {
837
- const image = new Image(properties);
838
- if ((!properties.width || !properties.height) && typeof properties.source === 'string') {
839
- try {
840
- await image.loadImageDataForDimensions();
841
- }
842
- catch (error) {
843
- }
487
+ toXML() {
488
+ const isFloating = this.isFloating();
489
+ const extent = XMLBuilder_1.XMLBuilder.cxCy('extent', this.width, this.height);
490
+ const blip = XMLBuilder_1.XMLBuilder.a('blip', { 'r:embed': this.relationshipId });
491
+ const spPrChildren = [extent];
492
+ spPrChildren.push(XMLBuilder_1.XMLBuilder.a('prstGeom', { prst: 'rect' }));
493
+ if (this.border) {
494
+ const pxToEmu = 9525;
495
+ const widthEmu = this.border.width * pxToEmu;
496
+ const ln = XMLBuilder_1.XMLBuilder.a('ln', { w: widthEmu.toString() }, [
497
+ XMLBuilder_1.XMLBuilder.a('solidFill', undefined, [
498
+ XMLBuilder_1.XMLBuilder.a('srgbClr', { val: this.border.color })
499
+ ])
500
+ ]);
501
+ spPrChildren.push(ln);
502
+ }
503
+ const graphicData = XMLBuilder_1.XMLBuilder.a('graphicData', { uri: 'http://schemas.openxmlformats.org/drawingml/2006/picture' }, [
504
+ XMLBuilder_1.XMLBuilder.pic('pic', undefined, [
505
+ XMLBuilder_1.XMLBuilder.pic('nvPicPr', undefined, [
506
+ XMLBuilder_1.XMLBuilder.pic('cNvPr', { id: this.docPrId, name: this.name, descr: this.description }),
507
+ XMLBuilder_1.XMLBuilder.pic('cNvPicPr')
508
+ ]),
509
+ XMLBuilder_1.XMLBuilder.pic('blipFill', undefined, [blip]),
510
+ XMLBuilder_1.XMLBuilder.pic('spPr', undefined, spPrChildren)
511
+ ])
512
+ ]);
513
+ const graphic = XMLBuilder_1.XMLBuilder.a('graphic', undefined, [graphicData]);
514
+ if (isFloating) {
515
+ const positionHChildren = [];
516
+ if (this.position?.horizontal.alignment) {
517
+ positionHChildren.push(XMLBuilder_1.XMLBuilder.wp('align', undefined, [this.position.horizontal.alignment]));
518
+ }
519
+ else {
520
+ positionHChildren.push(XMLBuilder_1.XMLBuilder.wp('posOffset', undefined, [(this.position?.horizontal.offset || 0).toString()]));
521
+ }
522
+ const positionH = XMLBuilder_1.XMLBuilder.wp('positionH', { relativeFrom: this.position?.horizontal.anchor || 'page' }, positionHChildren);
523
+ const positionVChildren = [];
524
+ if (this.position?.vertical.alignment) {
525
+ positionVChildren.push(XMLBuilder_1.XMLBuilder.wp('align', undefined, [this.position.vertical.alignment]));
526
+ }
527
+ else {
528
+ positionVChildren.push(XMLBuilder_1.XMLBuilder.wp('posOffset', undefined, [(this.position?.vertical.offset || 0).toString()]));
529
+ }
530
+ const positionV = XMLBuilder_1.XMLBuilder.wp('positionV', { relativeFrom: this.position?.vertical.anchor || 'page' }, positionVChildren);
531
+ const anchorChildren = [
532
+ positionH,
533
+ positionV,
534
+ extent
535
+ ];
536
+ if (this.wrap) {
537
+ const wrapAttrs = {};
538
+ if (this.wrap.distanceTop !== undefined)
539
+ wrapAttrs.distT = this.wrap.distanceTop;
540
+ if (this.wrap.distanceBottom !== undefined)
541
+ wrapAttrs.distB = this.wrap.distanceBottom;
542
+ if (this.wrap.distanceLeft !== undefined)
543
+ wrapAttrs.distL = this.wrap.distanceLeft;
544
+ if (this.wrap.distanceRight !== undefined)
545
+ wrapAttrs.distR = this.wrap.distanceRight;
546
+ if (this.wrap.side)
547
+ wrapAttrs.wrapText = this.wrap.side;
548
+ let wrapElementName;
549
+ switch (this.wrap.type) {
550
+ case 'square':
551
+ wrapElementName = 'wrapSquare';
552
+ break;
553
+ case 'tight':
554
+ wrapElementName = 'wrapTight';
555
+ break;
556
+ case 'through':
557
+ wrapElementName = 'wrapThrough';
558
+ break;
559
+ case 'topAndBottom':
560
+ wrapElementName = 'wrapTopAndBottom';
561
+ break;
562
+ case 'none':
563
+ wrapElementName = 'wrapNone';
564
+ break;
565
+ default: wrapElementName = 'wrapSquare';
566
+ }
567
+ anchorChildren.push(XMLBuilder_1.XMLBuilder.wp(wrapElementName, wrapAttrs));
568
+ }
569
+ anchorChildren.push(XMLBuilder_1.XMLBuilder.wp('docPr', { id: this.docPrId, name: this.name, descr: this.description }));
570
+ anchorChildren.push(graphic);
571
+ return XMLBuilder_1.XMLBuilder.w('drawing', undefined, [
572
+ XMLBuilder_1.XMLBuilder.wp('anchor', {
573
+ behindDoc: this.anchor?.behindDoc ? 1 : 0,
574
+ locked: this.anchor?.locked ? 1 : 0,
575
+ layoutInCell: this.anchor?.layoutInCell ? 1 : 0,
576
+ allowOverlap: this.anchor?.allowOverlap ? 1 : 0,
577
+ relativeHeight: this.anchor?.relativeHeight
578
+ }, anchorChildren)
579
+ ]);
844
580
  }
845
- else if ((!properties.width || !properties.height) && Buffer.isBuffer(properties.source)) {
846
- await image.loadImageDataForDimensions();
581
+ else {
582
+ return XMLBuilder_1.XMLBuilder.w('drawing', undefined, [
583
+ XMLBuilder_1.XMLBuilder.wp('inline', undefined, [
584
+ extent,
585
+ XMLBuilder_1.XMLBuilder.wp('docPr', { id: this.docPrId, name: this.name, descr: this.description }),
586
+ graphic
587
+ ])
588
+ ]);
847
589
  }
848
- return image;
849
590
  }
850
591
  }
851
592
  exports.Image = Image;