From 6a6bf0583cadb81e34a866f07490ffa1e475e6fa Mon Sep 17 00:00:00 2001 From: Skyler Moosman <8845503+TheMooseman@users.noreply.github.com> Date: Fri, 1 May 2026 12:40:23 -0700 Subject: [PATCH 1/5] basic tiff viewer --- packages/tiff/README.md | 27 +++ packages/tiff/example/README.md | 21 ++ packages/tiff/package.json | 42 ++++ packages/tiff/src/index.ts | 276 ++++++++++++++++++++++++ packages/tiff/tsconfig.json | 14 ++ pnpm-lock.yaml | 42 ++++ site/package.json | 1 + site/src/content/docs/examples/tiff.mdx | 24 +++ site/src/examples/tiff/tiff-demo.tsx | 92 ++++++++ 9 files changed, 539 insertions(+) create mode 100644 packages/tiff/README.md create mode 100644 packages/tiff/example/README.md create mode 100644 packages/tiff/package.json create mode 100644 packages/tiff/src/index.ts create mode 100644 packages/tiff/tsconfig.json create mode 100644 site/src/content/docs/examples/tiff.mdx create mode 100644 site/src/examples/tiff/tiff-demo.tsx diff --git a/packages/tiff/README.md b/packages/tiff/README.md new file mode 100644 index 00000000..2ab4e0f3 --- /dev/null +++ b/packages/tiff/README.md @@ -0,0 +1,27 @@ +# @vis/tiff + +A small browser package to decode TIFF images and render them with WebGPU (with a 2D canvas fallback). + +Usage: + +- Install in monorepo (pnpm): + +```bash +pnpm add -w @vis/tiff +``` + +- Simple browser usage: + +```ts +import { createTiffViewer } from '@vis/tiff'; + +const container = document.getElementById('viewer')!; +createTiffViewer(container, '/path/to/image.tiff'); +``` + +Build: + +```bash +pnpm -C packages/tiff install +pnpm -C packages/tiff build +``` diff --git a/packages/tiff/example/README.md b/packages/tiff/example/README.md new file mode 100644 index 00000000..b7b5a3ee --- /dev/null +++ b/packages/tiff/example/README.md @@ -0,0 +1,21 @@ +tiff example + +Steps to run: + +1. From repo root build the package: + +```bash +pnpm -w install +pnpm -C packages/tiff build +``` + +2. Serve the example folder with any static server (e.g. `npx serve` or `python -m http.server`): + +```bash +cd packages/tiff/example +npx serve . +# or +python -m http.server 8000 +``` + +3. Open the served `index.html` and update `sample.tiff` in `main.js` to point to a real TIFF file. diff --git a/packages/tiff/package.json b/packages/tiff/package.json new file mode 100644 index 00000000..4e773fa8 --- /dev/null +++ b/packages/tiff/package.json @@ -0,0 +1,42 @@ +{ + "name": "@alleninstitute/vis-tiff", + "version": "0.0.1", + "private": true, + "description": "A small WebGPU TIFF viewer for the browser", + "source": "src/index.ts", + "main": "dist/main.js", + "types": "dist/types.d.ts", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "typecheck": "tsc --noEmit", + "build": "parcel build --no-cache", + "dev": "parcel watch --port 1240", + "demo": "parcel serve example/index.html --port 1242", + "test": "vitest --watch", + "test:ci": "vitest run", + "coverage": "vitest run --coverage", + "changelog": "git-cliff -o changelog.md" + }, + "keywords": [ + "webgpu", + "tiff", + "viewer" + ], + "license": "BSD-3-Clause", + "dependencies": { + "tiff": "^7.1.3", + "webgpu-utils": "^0.2.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@webgpu/types": "^0.1.69" + }, + "repository": { + "type": "git", + "url": "https://github.com/AllenInstitute/vis.git" + }, + "packageManager": "pnpm@9.14.2" +} diff --git a/packages/tiff/src/index.ts b/packages/tiff/src/index.ts new file mode 100644 index 00000000..eb93808e --- /dev/null +++ b/packages/tiff/src/index.ts @@ -0,0 +1,276 @@ +// why does my normal import not work??? +/// +import { decode } from 'tiff'; + +type DecodedImage = { + width?: number; + height?: number; + shape?: number[]; + size?: { width: number; height: number }; + data?: Uint8Array | Uint8ClampedArray; + bits?: Uint8Array | Uint8ClampedArray; + getPixelsArray?: () => Uint8Array; + components?: number; + channels?: number; +}; + +function isDecodedImage(v: unknown): v is DecodedImage { + if (!v || typeof v !== 'object') return false; + const obj = v as Record; + return ( + typeof obj.width === 'number' || + Array.isArray(obj.shape) || + (typeof obj.size === 'object' && + obj.size !== null && + typeof (obj.size as Record).width === 'number') || + obj.data instanceof Uint8Array || + typeof obj.getPixelsArray === 'function' + ); +} + +export async function createTiffViewer( + container: HTMLElement, + url: string, +): Promise<{ canvas: HTMLCanvasElement; destroy: () => void }> { + const res = await fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`); + const buf = await res.arrayBuffer(); + + const { rgba, width, height } = await decodeTiff(buf); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + // fill parent container + canvas.style.width = '100%'; + canvas.style.height = '100%'; + container.appendChild(canvas); + + if (navigator.gpu) { + try { + const destroy = await renderWithWebGPU(canvas, rgba, width, height); + return { canvas, destroy }; + } catch (err) { + // biome-ignore lint/suspicious/noConsole: just warning them + console.warn('WebGPU rendering failed, falling back to 2D canvas', err); + } + } + + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('2D context not available'); + const packed = rgba.subarray(0, width * height * 4); + const clamped = new Uint8ClampedArray(packed); + const imageData = new ImageData(clamped, width, height); + ctx.putImageData(imageData, 0, 0); + const destroy = () => { + try { + if (canvas.parentElement) canvas.parentElement.removeChild(canvas); + } catch {} + }; + return { canvas, destroy }; +} + +async function decodeTiff(buf: ArrayBuffer): Promise<{ rgba: Uint8Array; width: number; height: number }> { + const input = new Uint8Array(buf); + const r = await decode(input); + const img = (Array.isArray(r) ? r[0] : r) as unknown; + // dumb typing on the tiff converter + if (!isDecodedImage(img)) throw new Error('No images decoded from TIFF'); + + const width = img.width ?? img.shape?.[0] ?? img.size?.width; + const height = img.height ?? img.shape?.[1] ?? img.size?.height; + if (!width || !height) throw new Error('Decoded image missing width/height'); + + const data: Uint8Array | Uint8ClampedArray | undefined = img.data ?? img.getPixelsArray?.() ?? img.bits; + if (!data) throw new Error('Decoded image missing pixel data'); + + const components = img.components ?? img.channels ?? 4; + + if (components === 4) { + return { rgba: new Uint8Array(data.buffer, data.byteOffset, width * height * 4), width, height }; + } + + const out = new Uint8Array(width * height * 4); + if (components === 3) { + for (let i = 0, j = 0; i < width * height; i++, j += 3) { + const o = i * 4; + out[o] = data[j]; + out[o + 1] = data[j + 1]; + out[o + 2] = data[j + 2]; + out[o + 3] = 255; + } + return { rgba: out, width, height }; + } + + if (components === 1) { + for (let i = 0; i < width * height; i++) { + const v = data[i]; + const o = i * 4; + out[o] = v; + out[o + 1] = v; + out[o + 2] = v; + out[o + 3] = 255; + } + return { rgba: out, width, height }; + } + + return { + rgba: new Uint8Array(data.buffer, data.byteOffset, Math.min(data.length, width * height * 4)), + width, + height, + }; +} + +async function renderWithWebGPU( + canvas: HTMLCanvasElement, + rgba: Uint8Array, + width: number, + height: number, +): Promise<() => void> { + const gpu = navigator.gpu as GPU; + const adapter = await gpu.requestAdapter(); + if (!adapter) throw new Error('No GPU adapter available'); + const device = await adapter.requestDevice(); + const context = canvas.getContext('webgpu') as GPUCanvasContext; + const format = navigator.gpu.getPreferredCanvasFormat ? navigator.gpu.getPreferredCanvasFormat() : 'bgra8unorm'; + + context.configure({ device, format, alphaMode: 'opaque' }); + + const texture = device.createTexture({ + size: { width, height }, + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, + }); + + // WebGPU requires bytesPerRow to be a multiple of 256 + const bytesPerPixel = 4; + const unpaddedBytesPerRow = width * bytesPerPixel; + const alignedBytesPerRow = Math.ceil(unpaddedBytesPerRow / 256) * 256; + + // rgba isnt being nice about types... + const rgbaData = rgba instanceof Uint8Array && rgba.buffer instanceof ArrayBuffer ? rgba : new Uint8Array(rgba); + + if (alignedBytesPerRow === unpaddedBytesPerRow) { + device.queue.writeTexture({ texture }, rgbaData, { bytesPerRow: unpaddedBytesPerRow }, { width, height }); + } else { + const padded = new Uint8Array(alignedBytesPerRow * height); + for (let row = 0; row < height; row++) { + const srcStart = row * unpaddedBytesPerRow; + const dstStart = row * alignedBytesPerRow; + padded.set(rgbaData.subarray(srcStart, srcStart + unpaddedBytesPerRow), dstStart); + } + device.queue.writeTexture( + { texture }, + padded, + { bytesPerRow: alignedBytesPerRow, rowsPerImage: height }, + { width, height }, + ); + } + + const shader = ` + @group(0) @binding(0) var samp: sampler; + @group(0) @binding(1) var tex: texture_2d; + + struct Uniforms { size: vec2 }; + @group(0) @binding(2) var uniforms: Uniforms; + + @vertex + fn vs(@builtin(vertex_index) vi : u32) -> @builtin(position) vec4 { + var positions = array, 3>( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0) + ); + return vec4(positions[vi], 0.0, 1.0); + } + + @fragment + fn fs(@builtin(position) fragCoord: vec4) -> @location(0) vec4 { + let uv = fragCoord.xy / uniforms.size; + return textureSample(tex, samp, uv); + } + `; + + const module = device.createShaderModule({ code: shader }); + + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { module, entryPoint: 'vs' }, + fragment: { module, entryPoint: 'fs', targets: [{ format }] }, + primitive: { topology: 'triangle-list' }, + }); + + const sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' }); + const uniformBuffer = device.createBuffer({ + size: 8, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([width, height])); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: sampler }, + { binding: 1, resource: texture.createView() }, + { binding: 2, resource: { buffer: uniformBuffer } }, + ], + }); + + function draw() { + const commandEncoder = device.createCommandEncoder(); + const textureView = context.getCurrentTexture().createView(); + const pass = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: textureView, + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3, 1, 0, 0); + pass.end(); + + device.queue.submit([commandEncoder.finish()]); + } + + // handle resizes + const dpr = typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1; + function updateCanvasSize(size?: { width: number; height: number }) { + const clientW = size?.width ?? canvas.clientWidth ?? width; + const clientH = size?.height ?? canvas.clientHeight ?? height; + const w = Math.max(1, Math.floor(clientW * dpr)); + const h = Math.max(1, Math.floor(clientH * dpr)); + if (canvas.width !== w || canvas.height !== h) { + canvas.width = w; + canvas.height = h; + device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([w, h])); + draw(); + } + } + + // draw after first frame + requestAnimationFrame(() => updateCanvasSize()); + const resizeObserver = new ResizeObserver((entries) => { + const e = entries[0]; + if (e.target !== canvas) return; + const r = e.contentRect; + requestAnimationFrame(() => updateCanvasSize({ width: r.width, height: r.height })); + }); + resizeObserver.observe(canvas); + + // draw in case layout is already complete + draw(); + + const destroy = () => { + resizeObserver.disconnect(); + texture.destroy(); + uniformBuffer.destroy(); + if (canvas.parentElement) canvas.parentElement.removeChild(canvas); + }; + + return destroy; +} diff --git a/packages/tiff/tsconfig.json b/packages/tiff/tsconfig.json new file mode 100644 index 00000000..6e49bf5f --- /dev/null +++ b/packages/tiff/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "paths": { + "~/*": ["./*"] + }, + "types": ["@webgpu/types"], + "moduleResolution": "Bundler", + "module": "es6", + "target": "es2024", + "lib": ["es2024", "DOM"] + }, + "include": ["./src/index.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c559ac82..9d60a1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,22 @@ importers: specifier: 4.17.24 version: 4.17.24 + packages/tiff: + dependencies: + tiff: + specifier: ^7.1.3 + version: 7.1.3 + webgpu-utils: + specifier: ^0.2.0 + version: 0.2.4 + devDependencies: + '@webgpu/types': + specifier: ^0.1.69 + version: 0.1.69 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + site: dependencies: '@alleninstitute/vis-core': @@ -132,6 +148,9 @@ importers: '@alleninstitute/vis-scatterbrain': specifier: workspace:* version: link:../packages/scatterbrain + '@alleninstitute/vis-tiff': + specifier: workspace:* + version: link:../packages/tiff '@astrojs/check': specifier: 0.9.6 version: 0.9.6(prettier@3.8.1)(typescript@5.9.3) @@ -1986,6 +2005,9 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@webgpu/types@0.1.69': + resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} + '@zarrita/storage@0.1.3': resolution: {integrity: sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==} @@ -2622,6 +2644,9 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + iobuffer@6.0.1: + resolution: {integrity: sha512-SZWYkWNfjIXIBYSDpXDYIgshqtbOPsi4lviawAEceR1Kqk+sHDlcQjWrzNQsii80AyBY0q5c8HCTNjqo74ul+Q==} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -3488,6 +3513,9 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + tiff@7.1.3: + resolution: {integrity: sha512-YEEq3fT++2pdta/9P/vGG4QRMdZQoe6W6JNaWnIi6NvAsbeNITwFCtmWwL/BZvOi+uo2I3ohyOkD3sZfme+c6g==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -3933,6 +3961,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webgpu-utils@0.2.4: + resolution: {integrity: sha512-nER0/yVy4tZZbR/j9ktmFnM0Nj7zOOgiE4jbtuHhwr3xf0jaW4bEkR89v8z+/LSFX1InKmEpj9al12wvLZO7Jg==} + which-pm-runs@1.1.0: resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} engines: {node: '>=4'} @@ -5918,6 +5949,8 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@webgpu/types@0.1.69': {} + '@zarrita/storage@0.1.3': dependencies: reference-spec-reader: 0.2.0 @@ -6768,6 +6801,8 @@ snapshots: inline-style-parser@0.2.4: {} + iobuffer@6.0.1: {} + iron-webcrypto@1.2.1: {} is-alphabetical@2.0.1: {} @@ -8023,6 +8058,11 @@ snapshots: term-size@2.2.1: {} + tiff@7.1.3: + dependencies: + fflate: 0.8.2 + iobuffer: 6.0.1 + tiny-inflate@1.0.3: {} tinybench@2.9.0: {} @@ -8351,6 +8391,8 @@ snapshots: web-namespaces@2.0.1: {} + webgpu-utils@0.2.4: {} + which-pm-runs@1.1.0: {} which@2.0.2: diff --git a/site/package.json b/site/package.json index 308577e8..a038a5a3 100644 --- a/site/package.json +++ b/site/package.json @@ -50,6 +50,7 @@ "@alleninstitute/vis-geometry": "workspace:*", "@alleninstitute/vis-omezarr": "workspace:*", "@alleninstitute/vis-scatterbrain": "workspace:*", + "@alleninstitute/vis-tiff": "workspace:*", "@astrojs/check": "0.9.6", "@astrojs/mdx": "4.3.13", "@astrojs/react": "4.4.2", diff --git a/site/src/content/docs/examples/tiff.mdx b/site/src/content/docs/examples/tiff.mdx new file mode 100644 index 00000000..efa240b2 --- /dev/null +++ b/site/src/content/docs/examples/tiff.mdx @@ -0,0 +1,24 @@ +--- +title: TIFF +description: A simple TIFF viewer demo using @alleninstitute/vis-tiff +--- + +import { Code, Aside } from '@astrojs/starlight/components'; +import { TiffDemo } from '../../../examples/tiff/tiff-demo.tsx'; +import tiffDemoCode from '../../../examples/tiff/tiff-demo.tsx?raw'; + +# TIFF + +This page shows a minimal TIFF viewer built with the `@alleninstitute/vis-tiff` package. + + + +## Live Demo + + + +## Example Code + + diff --git a/site/src/examples/tiff/tiff-demo.tsx b/site/src/examples/tiff/tiff-demo.tsx new file mode 100644 index 00000000..f4972ba5 --- /dev/null +++ b/site/src/examples/tiff/tiff-demo.tsx @@ -0,0 +1,92 @@ +import { useEffect, useRef, useState } from 'react'; +import { createTiffViewer } from '@alleninstitute/vis-tiff'; + +const DEFAULT_URLS = ['https://upload.wikimedia.org/wikipedia/commons/d/d8/Example.tiff']; + +export function TiffDemo() { + const containerRef = useRef(null); + const [inputUrl, setInputUrl] = useState(DEFAULT_URLS[0]); + const viewerRef = useRef<{ canvas: HTMLCanvasElement; destroy: () => void } | null>(null); + const [loading, setLoading] = useState(false); + + async function loadUrl(next: string) { + const container = containerRef.current; + if (!container) return; + viewerRef.current?.destroy(); + + setLoading(true); + try { + const handle = await createTiffViewer(container, next); + viewerRef.current = handle; + } catch (err) { + // biome-ignore lint/suspicious/noConsole: I'm erroring when I want + console.error('Failed to create TIFF viewer', err); + } finally { + setLoading(false); + } + } + + // biome-ignore lint/correctness/useExhaustiveDependencies: just run once + useEffect(() => { + loadUrl(DEFAULT_URLS[0]); + return () => { + try { + viewerRef.current?.destroy(); + } catch {} + }; + }, []); + + return ( +
+

TIFF Demo

+
+ setInputUrl(e.target.value)} style={{ width: '60%' }} /> + +
+
+
+ {loading && ( +
+ + Spinner + + + + +
+ )} +
+
+ ); +} From d28183d1c5b0c2aad3e0ab97ac9ba0be92bf9e98 Mon Sep 17 00:00:00 2001 From: Skyler Moosman <8845503+TheMooseman@users.noreply.github.com> Date: Fri, 1 May 2026 12:42:25 -0700 Subject: [PATCH 2/5] no double header --- site/src/examples/tiff/tiff-demo.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/site/src/examples/tiff/tiff-demo.tsx b/site/src/examples/tiff/tiff-demo.tsx index f4972ba5..4c6780fd 100644 --- a/site/src/examples/tiff/tiff-demo.tsx +++ b/site/src/examples/tiff/tiff-demo.tsx @@ -12,7 +12,7 @@ export function TiffDemo() { async function loadUrl(next: string) { const container = containerRef.current; if (!container) return; - viewerRef.current?.destroy(); + viewerRef.current?.destroy(); setLoading(true); try { @@ -38,10 +38,9 @@ export function TiffDemo() { return (
-

TIFF Demo

setInputUrl(e.target.value)} style={{ width: '60%' }} /> -
@@ -62,7 +61,7 @@ export function TiffDemo() { background: 'transparent', }} > - + Spinner Date: Fri, 1 May 2026 12:49:04 -0700 Subject: [PATCH 3/5] no tests, fix typing on rgba --- packages/tiff/package.json | 2 -- packages/tiff/src/index.ts | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/tiff/package.json b/packages/tiff/package.json index 4e773fa8..fe554a16 100644 --- a/packages/tiff/package.json +++ b/packages/tiff/package.json @@ -15,8 +15,6 @@ "build": "parcel build --no-cache", "dev": "parcel watch --port 1240", "demo": "parcel serve example/index.html --port 1242", - "test": "vitest --watch", - "test:ci": "vitest run", "coverage": "vitest run --coverage", "changelog": "git-cliff -o changelog.md" }, diff --git a/packages/tiff/src/index.ts b/packages/tiff/src/index.ts index eb93808e..912d8a55 100644 --- a/packages/tiff/src/index.ts +++ b/packages/tiff/src/index.ts @@ -147,11 +147,15 @@ async function renderWithWebGPU( const unpaddedBytesPerRow = width * bytesPerPixel; const alignedBytesPerRow = Math.ceil(unpaddedBytesPerRow / 256) * 256; - // rgba isnt being nice about types... - const rgbaData = rgba instanceof Uint8Array && rgba.buffer instanceof ArrayBuffer ? rgba : new Uint8Array(rgba); + const rgbaData = new Uint8Array(rgba); if (alignedBytesPerRow === unpaddedBytesPerRow) { - device.queue.writeTexture({ texture }, rgbaData, { bytesPerRow: unpaddedBytesPerRow }, { width, height }); + device.queue.writeTexture( + { texture }, + rgbaData, + { bytesPerRow: unpaddedBytesPerRow }, + { width, height }, + ); } else { const padded = new Uint8Array(alignedBytesPerRow * height); for (let row = 0; row < height; row++) { From 301bccbef7cd6262ce8a9c5d516083e01ca07f10 Mon Sep 17 00:00:00 2001 From: Skyler Moosman <8845503+TheMooseman@users.noreply.github.com> Date: Fri, 1 May 2026 12:58:47 -0700 Subject: [PATCH 4/5] fmt --- packages/tiff/src/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/tiff/src/index.ts b/packages/tiff/src/index.ts index 912d8a55..58130988 100644 --- a/packages/tiff/src/index.ts +++ b/packages/tiff/src/index.ts @@ -150,12 +150,7 @@ async function renderWithWebGPU( const rgbaData = new Uint8Array(rgba); if (alignedBytesPerRow === unpaddedBytesPerRow) { - device.queue.writeTexture( - { texture }, - rgbaData, - { bytesPerRow: unpaddedBytesPerRow }, - { width, height }, - ); + device.queue.writeTexture({ texture }, rgbaData, { bytesPerRow: unpaddedBytesPerRow }, { width, height }); } else { const padded = new Uint8Array(alignedBytesPerRow * height); for (let row = 0; row < height; row++) { From add7f16e065f1c145c03cf35cd66e9d4b40e770e Mon Sep 17 00:00:00 2001 From: Skyler Moosman <8845503+TheMooseman@users.noreply.github.com> Date: Mon, 4 May 2026 10:19:14 -0700 Subject: [PATCH 5/5] update readme --- packages/tiff/example/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/tiff/example/README.md b/packages/tiff/example/README.md index b7b5a3ee..f1da1a9e 100644 --- a/packages/tiff/example/README.md +++ b/packages/tiff/example/README.md @@ -9,13 +9,11 @@ pnpm -w install pnpm -C packages/tiff build ``` -2. Serve the example folder with any static server (e.g. `npx serve` or `python -m http.server`): +2. Serve the example folder with any static server (e.g. `npx serve`): ```bash cd packages/tiff/example npx serve . -# or -python -m http.server 8000 ``` 3. Open the served `index.html` and update `sample.tiff` in `main.js` to point to a real TIFF file.