From f0f0bac57a1f8790a9866fb4e476ac0a6e86b345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oc=C3=A9ane=20Patiny?= Date: Fri, 22 Dec 2023 12:26:11 +0100 Subject: [PATCH] feat: add `pages` option (#47) * refactor: remove I for interface and export TiffIfd Closes: #37 BREAKING CHANGE: removed `firstImage` option, use `pages` option instead. Closes: #46 --- src/__tests__/decode.test.ts | 25 +++++++++++++++-- src/index.ts | 6 ++--- src/tiffDecoder.ts | 52 ++++++++++++++++++++++++++++-------- src/types.ts | 7 +++-- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/__tests__/decode.test.ts b/src/__tests__/decode.test.ts index 0b8d9aa..6c2484b 100644 --- a/src/__tests__/decode.test.ts +++ b/src/__tests__/decode.test.ts @@ -225,7 +225,7 @@ test('should decode RGB 8bit data with pre-multiplied alpha and lost precision', }); test('should decode with onlyFirst', () => { - const result = decode(readImage('grey8.tif'), { onlyFirst: true }); + const result = decode(readImage('grey8.tif'), { pages: [0] }); expect(result[0]).toHaveProperty('data'); }); @@ -236,7 +236,7 @@ test('should omit data', () => { test('should read exif data', () => { const result = decode(readImage('grey8.tif'), { - onlyFirst: true, + pages: [0], ignoreImageData: true, }); // @ts-ignore @@ -256,6 +256,27 @@ test('should decode stacks', () => { } }); +test('specify pages to decode', () => { + const decoded = decode(stack, { pages: [0, 2, 4, 6, 8] }); + expect(decoded).toHaveLength(5); + for (const image of decoded) { + expect(image.width).toBe(128); + expect(image.height).toBe(128); + } +}); + +test('should throw if pages invalid', () => { + expect(() => decode(stack, { pages: [-1] })).toThrow( + 'Index -1 is invalid. Must be a positive integer.', + ); + expect(() => decode(stack, { pages: [0.5] })).toThrow( + 'Index 0.5 is invalid. Must be a positive integer.', + ); + expect(() => decode(stack, { pages: [20] })).toThrow( + 'Index 20 is out of bounds. The stack only contains 10 images.', + ); +}); + test('should decode palette', () => { const decoded = decode(readImage('palette.tif')); expect(decoded).toHaveLength(1); diff --git a/src/index.ts b/src/index.ts index 77f5829..aa6fc45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import TIFFDecoder from './tiffDecoder'; import TiffIfd from './tiffIfd'; -import { BufferType, IDecodeOptions } from './types'; +import { BufferType, DecodeOptions } from './types'; -function decodeTIFF(data: BufferType, options?: IDecodeOptions): TiffIfd[] { +function decodeTIFF(data: BufferType, options?: DecodeOptions): TiffIfd[] { const decoder = new TIFFDecoder(data); return decoder.decode(options); } @@ -17,4 +17,4 @@ function pageCount(data: BufferType): number { return decoder.pageCount; } -export { decodeTIFF as decode, isMultiPage, pageCount, IDecodeOptions }; +export { decodeTIFF as decode, isMultiPage, pageCount, DecodeOptions, TiffIfd }; diff --git a/src/tiffDecoder.ts b/src/tiffDecoder.ts index 0889430..56c95d8 100644 --- a/src/tiffDecoder.ts +++ b/src/tiffDecoder.ts @@ -9,15 +9,14 @@ import IFD from './ifd'; import { getByteLength, readData } from './ifdValue'; import { decompressLzw } from './lzw'; import TiffIfd from './tiffIfd'; -import { BufferType, IDecodeOptions, IFDKind, DataArray } from './types'; +import { BufferType, DecodeOptions, IFDKind, DataArray } from './types'; import { decompressZlib } from './zlib'; -const defaultOptions: IDecodeOptions = { +const defaultOptions: DecodeOptions = { ignoreImageData: false, - onlyFirst: false, }; -interface IInternalOptions extends IDecodeOptions { +interface InternalOptions extends DecodeOptions { kind?: IFDKind; } @@ -58,15 +57,35 @@ export default class TIFFDecoder extends IOBuffer { throw unsupported('ifdCount', c); } - public decode(options: IDecodeOptions = {}): TiffIfd[] { + public decode(options: DecodeOptions = {}): TiffIfd[] { + const { pages } = options; + checkPages(pages); + + const maxIndex = pages ? Math.max(...pages) : Infinity; + options = Object.assign({}, defaultOptions, options); const result = []; this.decodeHeader(); + let index = 0; while (this._nextIFD) { - result.push(this.decodeIFD(options, true)); - if (options.onlyFirst) { - return [result[0]]; + if (pages) { + if (pages.includes(index)) { + result.push(this.decodeIFD(options, true)); + } else { + this.decodeIFD({ ignoreImageData: true }, true); + } + if (index === maxIndex) { + break; + } + } else { + result.push(this.decodeIFD(options, true)); } + index++; + } + if (index < maxIndex && maxIndex !== Infinity) { + throw new RangeError( + `Index ${maxIndex} is out of bounds. The stack only contains ${index} images.`, + ); } return result; } @@ -91,9 +110,9 @@ export default class TIFFDecoder extends IOBuffer { this._nextIFD = this.readUint32(); } - private decodeIFD(options: IInternalOptions, tiff: true): TiffIfd; - private decodeIFD(options: IInternalOptions, tiff: false): IFD; - private decodeIFD(options: IInternalOptions, tiff: boolean): TiffIfd | IFD { + private decodeIFD(options: InternalOptions, tiff: true): TiffIfd; + private decodeIFD(options: InternalOptions, tiff: false): IFD; + private decodeIFD(options: InternalOptions, tiff: boolean): TiffIfd | IFD { this.seek(this._nextIFD); let ifd: TiffIfd | IFD; @@ -380,3 +399,14 @@ function fillFloat32( function unsupported(type: string, value: any): Error { return new Error(`Unsupported ${type}: ${value}`); } +function checkPages(pages: number[] | undefined) { + if (pages) { + for (let page of pages) { + if (page < 0 || Number.isInteger(page) === false) { + throw new RangeError( + `Index ${page} is invalid. Must be a positive integer.`, + ); + } + } + } +} diff --git a/src/types.ts b/src/types.ts index 6b49645..e022494 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,9 +2,12 @@ import { IOBuffer } from 'iobuffer'; export type BufferType = ArrayBufferLike | ArrayBufferView | IOBuffer | Buffer; -export interface IDecodeOptions { +export interface DecodeOptions { ignoreImageData?: boolean; - onlyFirst?: boolean; + /** + * Specify the indices of the pages to decode in case of a multi-page TIFF. + */ + pages?: number[]; } export type IFDKind = 'standard' | 'exif' | 'gps';