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.
@@ -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 }