node-pptx-templater 1.0.17 → 1.0.19
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 +178 -4
- package/package.json +1 -1
- package/src/core/OutputWriter.js +12 -14
- package/src/core/PPTXTemplater.js +205 -5
- package/src/managers/ChartManager.js +0 -3
- package/src/managers/ImageManager.js +14 -18
- package/src/managers/MediaManager.js +72 -28
- package/src/managers/ShapeManager.js +19 -28
- package/src/managers/SlideManager.js +247 -4
- package/src/managers/TableManager.js +56 -87
- package/src/managers/ZOrderManager.js +0 -5
- package/src/managers/ZipManager.js +120 -14
- package/src/managers/charts/ChartWorkbookUpdater.js +1 -1
- package/src/utils/contentTypesHelper.js +6 -9
- package/src/utils/imageMetadata.js +227 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview imageMetadata - Pure JS helper to read image dimensions without full file reads.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const { promisify } = require('util')
|
|
7
|
+
const openAsync = promisify(fs.open)
|
|
8
|
+
const readAsync = promisify(fs.read)
|
|
9
|
+
const closeAsync = promisify(fs.close)
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads image dimensions and aspect ratio from a file path or buffer.
|
|
13
|
+
*
|
|
14
|
+
* @param {string|Buffer} source - File path or image Buffer.
|
|
15
|
+
* @returns {Promise<{ width: number, height: number, aspectRatio: number, type: string }>}
|
|
16
|
+
*/
|
|
17
|
+
async function getImageMetadata(source) {
|
|
18
|
+
let buffer
|
|
19
|
+
const isBuffer = Buffer.isBuffer(source) || source instanceof Uint8Array
|
|
20
|
+
|
|
21
|
+
if (isBuffer) {
|
|
22
|
+
buffer = Buffer.isBuffer(source) ? source : Buffer.from(source)
|
|
23
|
+
} else if (typeof source === 'string') {
|
|
24
|
+
// Read only the first 8KB of the file
|
|
25
|
+
let fd
|
|
26
|
+
try {
|
|
27
|
+
fd = await openAsync(source, 'r')
|
|
28
|
+
const tempBuffer = Buffer.alloc(8192)
|
|
29
|
+
const { bytesRead } = await readAsync(fd, tempBuffer, 0, 8192, 0)
|
|
30
|
+
buffer = tempBuffer.subarray(0, bytesRead)
|
|
31
|
+
} finally {
|
|
32
|
+
if (fd !== undefined) {
|
|
33
|
+
await closeAsync(fd).catch(() => {})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
throw new Error('Unsupported image source type')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (buffer.length < 4) {
|
|
41
|
+
throw new Error('Image file is too small or corrupt')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Detect image type by magic bytes
|
|
45
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
|
46
|
+
return parsePng(buffer)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
50
|
+
return parseJpeg(buffer)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
|
54
|
+
return parseGif(buffer)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
|
|
58
|
+
return parseBmp(buffer)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for SVG (starts with XML declaration or <svg)
|
|
62
|
+
const textContent = buffer.toString('utf8').trim()
|
|
63
|
+
if (
|
|
64
|
+
textContent.startsWith('<svg') ||
|
|
65
|
+
textContent.includes('<svg') ||
|
|
66
|
+
textContent.startsWith('<?xml')
|
|
67
|
+
) {
|
|
68
|
+
return parseSvg(textContent)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error('Unsupported image format or unrecognized signature')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parsePng(buffer) {
|
|
75
|
+
if (buffer.length < 24) {
|
|
76
|
+
throw new Error('PNG header too short')
|
|
77
|
+
}
|
|
78
|
+
// Width is at offset 16 (4 bytes, big endian)
|
|
79
|
+
const width = buffer.readUInt32BE(16)
|
|
80
|
+
// Height is at offset 20 (4 bytes, big endian)
|
|
81
|
+
const height = buffer.readUInt32BE(20)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
width,
|
|
85
|
+
height,
|
|
86
|
+
aspectRatio: width / height,
|
|
87
|
+
type: 'png',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseGif(buffer) {
|
|
92
|
+
if (buffer.length < 10) {
|
|
93
|
+
throw new Error('GIF header too short')
|
|
94
|
+
}
|
|
95
|
+
// Width is at offset 6 (2 bytes, little endian)
|
|
96
|
+
const width = buffer.readUInt16LE(6)
|
|
97
|
+
// Height is at offset 8 (2 bytes, little endian)
|
|
98
|
+
const height = buffer.readUInt16LE(8)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
width,
|
|
102
|
+
height,
|
|
103
|
+
aspectRatio: width / height,
|
|
104
|
+
type: 'gif',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseBmp(buffer) {
|
|
109
|
+
if (buffer.length < 26) {
|
|
110
|
+
throw new Error('BMP header too short')
|
|
111
|
+
}
|
|
112
|
+
// Width is at offset 18 (4 bytes, little endian)
|
|
113
|
+
const width = buffer.readInt32LE(18)
|
|
114
|
+
// Height is at offset 22 (4 bytes, little endian)
|
|
115
|
+
const height = buffer.readInt32LE(22)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
width: Math.abs(width),
|
|
119
|
+
height: Math.abs(height),
|
|
120
|
+
aspectRatio: Math.abs(width) / Math.abs(height),
|
|
121
|
+
type: 'bmp',
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseJpeg(buffer) {
|
|
126
|
+
let offset = 2 // Skip SOI marker (FF D8)
|
|
127
|
+
|
|
128
|
+
while (offset < buffer.length - 8) {
|
|
129
|
+
// Check marker signature
|
|
130
|
+
if (buffer[offset] !== 0xff) {
|
|
131
|
+
// Not a valid marker, search next FF
|
|
132
|
+
offset++
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skip extra FF padding
|
|
137
|
+
while (buffer[offset] === 0xff && offset < buffer.length) {
|
|
138
|
+
offset++
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (offset >= buffer.length) break
|
|
142
|
+
|
|
143
|
+
const marker = buffer[offset]
|
|
144
|
+
offset++
|
|
145
|
+
|
|
146
|
+
// SOI, EOI, TEM have no length
|
|
147
|
+
if (marker === 0xd8 || marker === 0xd9 || marker === 0x01) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Read segment length (2 bytes, big endian)
|
|
152
|
+
const length = buffer.readUInt16BE(offset)
|
|
153
|
+
|
|
154
|
+
// Check SOF markers: C0-C3, C5-CB, CD-CF
|
|
155
|
+
const isSOF =
|
|
156
|
+
(marker >= 0xc0 && marker <= 0xc3) ||
|
|
157
|
+
(marker >= 0xc5 && marker <= 0xcb) ||
|
|
158
|
+
(marker >= 0xcd && marker <= 0xcf)
|
|
159
|
+
|
|
160
|
+
if (isSOF) {
|
|
161
|
+
// SOF structure:
|
|
162
|
+
// Offset 0: precision (1 byte)
|
|
163
|
+
// Offset 1: height (2 bytes, big endian)
|
|
164
|
+
// Offset 3: width (2 bytes, big endian)
|
|
165
|
+
const height = buffer.readUInt16BE(offset + 3)
|
|
166
|
+
const width = buffer.readUInt16BE(offset + 5)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
width,
|
|
170
|
+
height,
|
|
171
|
+
aspectRatio: width / height,
|
|
172
|
+
type: 'jpg',
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Advance to next marker
|
|
177
|
+
offset += length
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
throw new Error('Could not find JPEG SOF marker')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseSvg(text) {
|
|
184
|
+
// Try finding <svg ...> tag
|
|
185
|
+
const svgMatch = /<svg([^>]+)>/i.exec(text)
|
|
186
|
+
if (!svgMatch) {
|
|
187
|
+
throw new Error('Invalid SVG: missing <svg> tag')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const svgAttr = svgMatch[1]
|
|
191
|
+
|
|
192
|
+
const widthMatch = /width\s*=\s*["']([^"']+)["']/i.exec(svgAttr)
|
|
193
|
+
const heightMatch = /height\s*=\s*["']([^"']+)["']/i.exec(svgAttr)
|
|
194
|
+
const viewBoxMatch = /viewBox\s*=\s*["']([^"']+)["']/i.exec(svgAttr)
|
|
195
|
+
|
|
196
|
+
let width = 0
|
|
197
|
+
let height = 0
|
|
198
|
+
|
|
199
|
+
if (widthMatch) width = parseFloat(widthMatch[1])
|
|
200
|
+
if (heightMatch) height = parseFloat(heightMatch[1])
|
|
201
|
+
|
|
202
|
+
// If width/height missing or using units, fallback to viewBox
|
|
203
|
+
if ((!width || !height || isNaN(width) || isNaN(height)) && viewBoxMatch) {
|
|
204
|
+
const parts = viewBoxMatch[1].trim().split(/\s+/)
|
|
205
|
+
if (parts.length === 4) {
|
|
206
|
+
const vbWidth = parseFloat(parts[2])
|
|
207
|
+
const vbHeight = parseFloat(parts[3])
|
|
208
|
+
if (!isNaN(vbWidth) && !isNaN(vbHeight)) {
|
|
209
|
+
width = width || vbWidth
|
|
210
|
+
height = height || vbHeight
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Default fallbacks if everything fails
|
|
216
|
+
width = width || 800
|
|
217
|
+
height = height || 600
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
width,
|
|
221
|
+
height,
|
|
222
|
+
aspectRatio: width / height,
|
|
223
|
+
type: 'svg',
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { getImageMetadata }
|