fix: replace BMP img.src decoder with pure-JS BMP parser for Brave/Firefox cross-browser support

This commit is contained in:
minsung
2026-07-30 15:48:37 +09:00
parent be8d52fcd5
commit 963a90319e
+77 -37
View File
@@ -823,45 +823,85 @@ export class Viewer2D {
// If embedded image texture (BMP/DIB) is present, render it as a 3D Plane Mesh! // If embedded image texture (BMP/DIB) is present, render it as a 3D Plane Mesh!
if (imageDataUrl) { if (imageDataUrl) {
const img = new Image(); try {
img.onload = () => { // Pure-JS BMP decoder — works in ALL browsers (Brave/Firefox don't support data:image/bmp in <img>)
try { const decodeBmpDataUrl = (dataUrl) => {
const canvas = document.createElement('canvas'); const b64 = dataUrl.split(',')[1];
canvas.width = img.naturalWidth || img.width || 100; const bin = atob(b64);
canvas.height = img.naturalHeight || img.height || 100; const buf = new Uint8Array(bin.length);
const ctx = canvas.getContext('2d'); for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
if (ctx) {
ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imgData.data;
// Ensure all alpha values are opaque (255) for GDI 32bpp BMPs
for (let i = 3; i < pixels.length; i += 4) {
pixels[i] = 255;
}
ctx.putImageData(imgData, 0, 0);
const texture = new THREE.CanvasTexture(canvas); const dv = new DataView(buf.buffer);
texture.colorSpace = THREE.SRGBColorSpace; // BITMAPFILEHEADER (14 bytes)
texture.needsUpdate = true; const sig = String.fromCharCode(buf[0], buf[1]);
const planeGeom = new THREE.PlaneGeometry(width, Math.abs(height)); if (sig !== 'BM') throw new Error('Not a BMP file');
const planeMat = new THREE.MeshBasicMaterial({ const pixelOffset = dv.getUint32(10, true);
map: texture, // BITMAPINFOHEADER (40 bytes, starts at offset 14)
side: THREE.DoubleSide, const bmpWidth = dv.getInt32(18, true);
transparent: false, const bmpHeight = dv.getInt32(22, true); // positive = bottom-up
}); const bitCount = dv.getUint16(28, true);
const mesh = new THREE.Mesh(planeGeom, planeMat); const absH = Math.abs(bmpHeight);
mesh.position.set(minX + width / 2, minY + Math.abs(height) / 2, z + 10); const flipY = bmpHeight > 0; // bottom-up storage
this._group.add(mesh);
this._renderer.render(this._scene, this._camera); const cvs = document.createElement('canvas');
cvs.width = bmpWidth;
cvs.height = absH;
const ctx2 = cvs.getContext('2d');
const imgData = ctx2.createImageData(bmpWidth, absH);
const px = imgData.data;
if (bitCount === 32) {
const bpr = bmpWidth * 4;
for (let y = 0; y < absH; y++) {
const srcRow = flipY ? (absH - 1 - y) : y;
const srcOff = pixelOffset + srcRow * bpr;
const dstOff = y * bmpWidth * 4;
for (let x = 0; x < bmpWidth; x++) {
const s = srcOff + x * 4;
const d = dstOff + x * 4;
px[d] = buf[s + 2]; // R (BMP stores BGR)
px[d + 1] = buf[s + 1]; // G
px[d + 2] = buf[s]; // B
px[d + 3] = 255; // A — GDI 32bpp has zeroed alpha padding
}
}
} else if (bitCount === 24) {
const stride = Math.ceil(bmpWidth * 3 / 4) * 4; // row padded to 4 bytes
for (let y = 0; y < absH; y++) {
const srcRow = flipY ? (absH - 1 - y) : y;
const srcOff = pixelOffset + srcRow * stride;
const dstOff = y * bmpWidth * 4;
for (let x = 0; x < bmpWidth; x++) {
const s = srcOff + x * 3;
const d = dstOff + x * 4;
px[d] = buf[s + 2];
px[d + 1] = buf[s + 1];
px[d + 2] = buf[s];
px[d + 3] = 255;
}
}
} }
} catch (err) { ctx2.putImageData(imgData, 0, 0);
console.error('Failed rendering CanvasTexture for OLE image:', err); return cvs;
} };
};
img.onerror = (err) => { const bmpCanvas = decodeBmpDataUrl(imageDataUrl);
console.error('Failed loading OLE imageDataUrl:', err); const texture = new THREE.CanvasTexture(bmpCanvas);
}; texture.colorSpace = THREE.SRGBColorSpace;
img.src = imageDataUrl; texture.needsUpdate = true;
const planeGeom = new THREE.PlaneGeometry(width, Math.abs(height));
const planeMat = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: false,
});
const mesh = new THREE.Mesh(planeGeom, planeMat);
mesh.position.set(minX + width / 2, minY + Math.abs(height) / 2, z + 10);
this._group.add(mesh);
this._renderer.render(this._scene, this._camera);
} catch (err) {
console.error('Failed rendering CanvasTexture for OLE image:', err);
}
} else { } else {
// Draw diagonal cross + overlay label fallback // Draw diagonal cross + overlay label fallback
pushSeg(minX, minY, maxX, maxY, z, color); pushSeg(minX, minY, maxX, maxY, z, color);