From 8106504e89dbd9fae022817629d931732ab63061 Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Fri, 16 Oct 2026 14:02:05 +1300 Subject: [PATCH] feat: add int16 support --- .../__tests__/pipeline.color.ramp.test.ts | 18 +++++++++ .../__tests__/pipeline.resize.test.ts | 40 +++++++++++++++++++ .../tiler-sharp/src/pipeline/decompressor.ts | 11 ++++- .../src/pipeline/decompressor.zstd.ts | 6 ++- .../src/pipeline/pipeline.color.ramp.ts | 1 + .../src/pipeline/pipeline.resize.ts | 8 ++++ 6 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/tiler-sharp/src/pipeline/__tests__/pipeline.color.ramp.test.ts b/packages/tiler-sharp/src/pipeline/__tests__/pipeline.color.ramp.test.ts index 1d987a29c..cfa88fc9d 100644 --- a/packages/tiler-sharp/src/pipeline/__tests__/pipeline.color.ramp.test.ts +++ b/packages/tiler-sharp/src/pipeline/__tests__/pipeline.color.ramp.test.ts @@ -111,4 +111,22 @@ describe('pipeline.color-ramp', () => { assert.equal(String(output.pixels.slice(4, 8)), '128,128,128,255'); assert.equal(String(output.pixels.slice(8, 12)), '255,255,255,255'); }); + + it('should color-ramp an int16', async () => { + const bytes: DecompressedInterleaved = { + pixels: new Int16Array([-(2 ** 15), 0, 2 ** 15 - 1]), + depth: 'int16', + channels: 1, + width: 3, + height: 1, + }; + + const output = await PipelineColorRamp.process(FakeComp, bytes); + + assert.equal(output.channels, 4); + + assert.equal(String(output.pixels.slice(0, 4)), '0,0,0,255'); + assert.equal(String(output.pixels.slice(4, 8)), '128,128,128,255'); + assert.equal(String(output.pixels.slice(8, 12)), '255,255,255,255'); + }); }); diff --git a/packages/tiler-sharp/src/pipeline/__tests__/pipeline.resize.test.ts b/packages/tiler-sharp/src/pipeline/__tests__/pipeline.resize.test.ts index 939939156..3f5217b6b 100644 --- a/packages/tiler-sharp/src/pipeline/__tests__/pipeline.resize.test.ts +++ b/packages/tiler-sharp/src/pipeline/__tests__/pipeline.resize.test.ts @@ -23,9 +23,49 @@ describe('resize-bilinear', () => { // All values should be rounded to 1 and not truncated down to 0 assert.ok(ret.pixels.every((f) => f === 1)); }); + + it('should round numbers when working with int16 arrays and preserve negative values', () => { + const ret = resizeBilinear( + { + pixels: new Int16Array([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]), + depth: 'int16', + width: 4, + height: 4, + channels: 1, + }, + { source: { width: 4, height: 4 } } as unknown as CompositionTiff, + { x: 0, y: 0, width: 1, height: 1 }, + { width: 256, height: 256, scale: 123.123123123 }, + ); + + assert.ok(ret.pixels instanceof Int16Array); + // All values should be rounded to -1 and not truncated towards zero + assert.ok(ret.pixels.every((f) => f === -1)); + }); }); describe('apply-crop', () => { + it('should apply a crop to a signed int16 array preserving negative values', () => { + const ret = applyCrop( + { + pixels: new Int16Array([-1, -1, -1, -1, -2, -2, -2, -2, -3, -3, -3, -3, -4, -4, -4, -4]), + depth: 'int16', + width: 4, + height: 4, + channels: 1, + }, + { width: 2, height: 3, x: 0, y: 0 }, + ); + + assert.ok(ret.pixels instanceof Int16Array); + assert.equal(ret.width, 2); + assert.equal(ret.height, 3); + assert.equal(ret.channels, 1); + assert.equal(ret.depth, 'int16'); + + assert.deepEqual(ret.pixels, new Int16Array([-1, -1, -2, -2, -3, -3])); + }); + it('should apply a crop', () => { const ret = applyCrop( { diff --git a/packages/tiler-sharp/src/pipeline/decompressor.ts b/packages/tiler-sharp/src/pipeline/decompressor.ts index e0b5e368b..2e8e08122 100644 --- a/packages/tiler-sharp/src/pipeline/decompressor.ts +++ b/packages/tiler-sharp/src/pipeline/decompressor.ts @@ -24,6 +24,14 @@ export interface DecompressedInterleavedUint16 { height: number; } +export interface DecompressedInterleavedInt16 { + pixels: Int16Array; + depth: 'int16'; + channels: number; + width: number; + height: number; +} + export interface DecompressedInterleavedUint8 { pixels: Uint8Array | Uint8ClampedArray; depth: 'uint8'; @@ -37,7 +45,8 @@ export type DecompressedInterleaved = | DecompressedInterleavedFloat | DecompressedInterleavedUint8 | DecompressedInterleavedUint16 - | DecompressedInterleavedUint32; + | DecompressedInterleavedUint32 + | DecompressedInterleavedInt16; export type TiffTileId = { imageId: number; x: number; y: number }; export interface Decompressor { diff --git a/packages/tiler-sharp/src/pipeline/decompressor.zstd.ts b/packages/tiler-sharp/src/pipeline/decompressor.zstd.ts index 5d96e754e..0eac6a388 100644 --- a/packages/tiler-sharp/src/pipeline/decompressor.zstd.ts +++ b/packages/tiler-sharp/src/pipeline/decompressor.zstd.ts @@ -29,11 +29,12 @@ function applyPredictor(data: DecompressedInterleaved, predictor?: number): Deco modulo = 2 ** 8; break; case 'uint16': + case 'int16': modulo = 2 ** 16; break; } - if (modulo === -1) throw new Error('Only unit8 or uint16 predictor supported'); + if (modulo === -1) throw new Error('Only unit8, uint16 or int16 predictor supported'); for (let y = 0; y < data.height; y++) { const offset = y * data.channels * data.width; @@ -88,6 +89,9 @@ function createDecompressedInterleaved( if (sampleLength === 16) return { pixels: new Uint16Array(bytes), depth: 'uint16', channels, width, height }; if (sampleLength === 32) return { pixels: new Uint32Array(bytes), depth: 'uint32', channels, width, height }; throw new Error(`Only uint8,uint16,uint32 supported: ${sampleLength} from: ${tiff.source.url.href}`); + case SampleFormat.Int: + if (sampleLength === 16) return { pixels: new Int16Array(bytes), depth: 'int16', channels, width, height }; + throw new Error(`Only int16 supported: ${sampleLength} from: ${tiff.source.url.href}`); } throw new Error('Unknown sample/bit combination: ' + tiff.source.url.href); } diff --git a/packages/tiler-sharp/src/pipeline/pipeline.color.ramp.ts b/packages/tiler-sharp/src/pipeline/pipeline.color.ramp.ts index b0a00a1da..ad487bd01 100644 --- a/packages/tiler-sharp/src/pipeline/pipeline.color.ramp.ts +++ b/packages/tiler-sharp/src/pipeline/pipeline.color.ramp.ts @@ -12,6 +12,7 @@ export const Ramps: Record = { uint8: new GreyScale(0, 255), uint16: new GreyScale(0, 2 ** 16 - 1), uint32: new GreyScale(0, 2 ** 32 - 1), + int16: new GreyScale(-(2 ** 15), 2 ** 15 - 1), }; function getRamp(data: DecompressedInterleaved, ctx?: PipelineColorRampArgs): Colorizer { diff --git a/packages/tiler-sharp/src/pipeline/pipeline.resize.ts b/packages/tiler-sharp/src/pipeline/pipeline.resize.ts index f41965d29..7d83ce951 100644 --- a/packages/tiler-sharp/src/pipeline/pipeline.resize.ts +++ b/packages/tiler-sharp/src/pipeline/pipeline.resize.ts @@ -147,6 +147,14 @@ function getOutputBuffer(source: DecompressedInterleaved, target: Size): Decompr depth: source.depth, channels: source.channels, }; + case 'int16': + return { + pixels: new Int16Array(target.width * target.height * source.channels), + width: target.width, + height: target.height, + depth: source.depth, + channels: source.channels, + }; } }