diff --git a/src/viewer2d/Viewer2D.js b/src/viewer2d/Viewer2D.js index 14df97a..458af4b 100644 --- a/src/viewer2d/Viewer2D.js +++ b/src/viewer2d/Viewer2D.js @@ -823,45 +823,85 @@ export class Viewer2D { // If embedded image texture (BMP/DIB) is present, render it as a 3D Plane Mesh! if (imageDataUrl) { - const img = new Image(); - img.onload = () => { - try { - const canvas = document.createElement('canvas'); - canvas.width = img.naturalWidth || img.width || 100; - canvas.height = img.naturalHeight || img.height || 100; - const ctx = canvas.getContext('2d'); - 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); + try { + // Pure-JS BMP decoder — works in ALL browsers (Brave/Firefox don't support data:image/bmp in ) + const decodeBmpDataUrl = (dataUrl) => { + const b64 = dataUrl.split(',')[1]; + const bin = atob(b64); + const buf = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i); - const texture = new THREE.CanvasTexture(canvas); - texture.colorSpace = THREE.SRGBColorSpace; - 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); + const dv = new DataView(buf.buffer); + // BITMAPFILEHEADER (14 bytes) + const sig = String.fromCharCode(buf[0], buf[1]); + if (sig !== 'BM') throw new Error('Not a BMP file'); + const pixelOffset = dv.getUint32(10, true); + // BITMAPINFOHEADER (40 bytes, starts at offset 14) + const bmpWidth = dv.getInt32(18, true); + const bmpHeight = dv.getInt32(22, true); // positive = bottom-up + const bitCount = dv.getUint16(28, true); + const absH = Math.abs(bmpHeight); + const flipY = bmpHeight > 0; // bottom-up storage + + 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) { - console.error('Failed rendering CanvasTexture for OLE image:', err); - } - }; - img.onerror = (err) => { - console.error('Failed loading OLE imageDataUrl:', err); - }; - img.src = imageDataUrl; + ctx2.putImageData(imgData, 0, 0); + return cvs; + }; + + const bmpCanvas = decodeBmpDataUrl(imageDataUrl); + const texture = new THREE.CanvasTexture(bmpCanvas); + texture.colorSpace = THREE.SRGBColorSpace; + 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 { // Draw diagonal cross + overlay label fallback pushSeg(minX, minY, maxX, maxY, z, color);