* Parse a STRETCHDIBITS record and extract the bitmap as ImageData.
( data: Uint8Array, view: DataView, offset: number, _recordSize: number )
| 193 | * Parse a STRETCHDIBITS record and extract the bitmap as ImageData. |
| 194 | */ |
| 195 | function parseStretchDibits( |
| 196 | data: Uint8Array, |
| 197 | view: DataView, |
| 198 | offset: number, |
| 199 | _recordSize: number |
| 200 | ): EmfContent | null { |
| 201 | // STRETCHDIBITS record layout (offsets from record start): |
| 202 | // 0: type(4), 4: size(4) |
| 203 | // 8: rclBounds (16 bytes) |
| 204 | // 24: xDest(4), 28: yDest(4) |
| 205 | // 32: xSrc(4), 36: ySrc(4) |
| 206 | // 40: cxSrc(4), 44: cySrc(4) |
| 207 | // 48: offBmiSrc(4), 52: cbBmiSrc(4) |
| 208 | // 56: offBitsSrc(4), 60: cbBitsSrc(4) |
| 209 | // 64: iUsageSrc(4), 68: dwRop(4) |
| 210 | // 72: cxDest(4), 76: cyDest(4) |
| 211 | if (offset + 80 > data.length) return null |
| 212 | |
| 213 | const offBmiSrc = view.getUint32(offset + 48, true) |
| 214 | const cbBmiSrc = view.getUint32(offset + 52, true) |
| 215 | const offBitsSrc = view.getUint32(offset + 56, true) |
| 216 | const cbBitsSrc = view.getUint32(offset + 60, true) |
| 217 | |
| 218 | if (cbBmiSrc === 0 || cbBitsSrc === 0) return null |
| 219 | |
| 220 | const bmiStart = offset + offBmiSrc |
| 221 | if (bmiStart + 40 > data.length) return null |
| 222 | |
| 223 | // Parse BITMAPINFOHEADER |
| 224 | const biWidth = view.getInt32(bmiStart + 4, true) |
| 225 | const biHeight = view.getInt32(bmiStart + 8, true) |
| 226 | const biBitCount = view.getUint16(bmiStart + 14, true) |
| 227 | const biCompression = view.getUint32(bmiStart + 16, true) |
| 228 | |
| 229 | // Only support uncompressed RGB bitmaps |
| 230 | if (biCompression !== BI_RGB) return null |
| 231 | if (biBitCount !== 24 && biBitCount !== 32) return null |
| 232 | |
| 233 | const width = Math.abs(biWidth) |
| 234 | const height = Math.abs(biHeight) |
| 235 | if (width === 0 || height === 0 || width > 8192 || height > 8192) return null |
| 236 | |
| 237 | const bitsStart = offset + offBitsSrc |
| 238 | if (bitsStart + cbBitsSrc > data.length) return null |
| 239 | |
| 240 | const bitsData = data.subarray(bitsStart, bitsStart + cbBitsSrc) |
| 241 | |
| 242 | // Negative height means top-down row order; positive means bottom-up |
| 243 | const topDown = biHeight < 0 |
| 244 | |
| 245 | const imageData = new ImageData(width, height) |
| 246 | const bytesPerPixel = biBitCount / 8 |
| 247 | // DIB rows are padded to 4-byte boundaries |
| 248 | const rowStride = Math.ceil((width * bytesPerPixel) / 4) * 4 |
| 249 | |
| 250 | for (let y = 0; y < height; y++) { |
| 251 | const srcRow = topDown ? y : height - 1 - y |
| 252 | const srcOffset = srcRow * rowStride |