Skip to content

Commit

Permalink
add files
Browse files Browse the repository at this point in the history
  • Loading branch information
KhooHaoYit committed Nov 9, 2023
1 parent a3f4023 commit a660ba4
Show file tree
Hide file tree
Showing 30 changed files with 2,124 additions and 2 deletions.
126 changes: 124 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,124 @@
# image-size
A stream based solution of https://github.com/image-size/image-size
# @khoohaoyit/image-size

> A stream based image size extractor in [Node](https://nodejs.org)
Features:
- Support variaty of formats
- Low memory footprint
- API compatible with [image-size](https://github.com/image-size/image-size)
- Fine control over when to stop parsing
- No dependancy

## Installation
```sh
npm install @khoohaoyit/image-size
```

## Usage

### Processing buffer
```ts
import { SizeExtractor } from "@khoohaoyit/image-size";
import { finished } from 'stream/promises';

const extractor = new SizeExtractor({ passthrough: false });
await finished(
extractor
.end(buffer)
);
console.log(extractor.imageSize); // Compatable with `image-size`
```

### Extract image size while writing to a file
```ts
import { SizeExtractor } from "@khoohaoyit/image-size";
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';

const extractor = new SizeExtractor({ passthrough: true });
await pipeline(
await fetch('https://github.githubassets.com/assets/gc_banner_dark-b73fa80c5473.png')
.then(res => res.body),
extractor,
createWriteStream('imageA.png'),
);
console.log(extractor.sizes);
```

### Stop processing once it has done parsing all formats
```ts
import { SizeExtractor } from "@khoohaoyit/image-size";
import { pipeline } from 'stream/promises';
import { createReadStream } from 'fs';

const extractor = new SizeExtractor({ passthrough: false });
const controller = new AbortController;
await pipeline(
createReadStream('imageA.png'),
extractor
.once('parseAllDone', () => controller.abort()),
{ signal: controller.signal },
).catch(err => {
if (controller.signal.aborted)
return;
throw err;
});
console.log(extractor.sizes);
```

## Library Comparison
| | [@khoohaoyit/image-size](https://github.com/KhooHaoYit/image-size.git) | [probe-image-size](https://github.com/nodeca/probe-image-size) | [image-size](https://github.com/image-size/image-size) |
| :-: | :-: | :-: | :-: |
| Stream based | Yes | Yes | No |
| Support sync | No | Yes | Yes |
| `dds` | Yes | No | Yes |
| `icns` | Yes | No | Yes |
| `j2c` | Yes | No | Yes |
| `jp2` | Yes | No | Yes |
| `ktx` | Yes | No | Yes |
| `pnm` | Yes | No | Yes |
| `tga` | Yes | No | Yes |
| `avif` | No | Yes | No |
| `heic` | No | Yes | No |
| `heif` | No | Yes | No |

The listed library on top all supports `ico`, `cur`, `bmp`, `gif`, `jpg`, `png`, `psd`, `svg`, `tiff`, and `webp`

## Documentation

### `new SizeExtractor(options)`
- `options.passthrough: boolean`
- Passes the input to output if `true`
- `options.whitelistFormats?: string[]`
- Whitelist specific formats
- `options.blacklistFormats?: string[]`
- Blacklist specific formats, ignored if `whitelistFormats` is set

### `SizeExtractor.imageSize`

The API compatible of [image-size](https://github.com/image-size/image-size) result

### `SizeExtractor.sizes`

The extracted sizes emitted from `'size'`

### Event: `'size'`
- `data: { width: number, height: number }`
- `format: string`

Emitted when a format has successfully extracted the image size

### Event: `'parseError'`
- `error: unknown`
- `format: string`

Emitted when a format encountered unexpected error while parsing

### Event: `'parseDone'`
- `format: string`

Emitted when a format has ended parsing

### Event: `'parseAllDone'`

Emitted when all formats has ended parsing
31 changes: 31 additions & 0 deletions lib/formats/[ico,cur].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ExtractSize } from "../utils/sizeExtractor";

const TYPE_ICON = 1;
const TYPE_CURSOR = 2;
const SIZE_HEADER = 2 + 2 + 2;
const SIZE_IMAGE_ENTRY = 1 + 1 + 1 + 1 + 2 + 2 + 4 + 4;

const extractSize = (targetImageType: number) => {
return async function* (stream) {
const reserved = await stream.readUInt16LE(0);
const imageCount = await stream.readUInt16LE(4);
if (reserved !== 0 || imageCount === 0)
return;
const imageType = await stream.readUInt16LE(2);
if (imageType !== targetImageType)
return;
for (
let amount = imageCount, offset = SIZE_HEADER
; amount
; --amount, offset += SIZE_IMAGE_ENTRY
) {
yield {
width: await stream.readUint8(offset) || 256,
height: await stream.readUint8(offset + 1) || 256,
};
}
} satisfies ExtractSize;
}

export const extractIcoSize = extractSize(TYPE_ICON);
export const extractCurSize = extractSize(TYPE_CURSOR);
10 changes: 10 additions & 0 deletions lib/formats/bmp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractBmpSize = async function* (stream) {
if (await stream.read(0, 2) + '' !== 'BM')
return;
return yield {
width: await stream.readUint32LE(18),
height: Math.abs(await stream.readInt32LE(22)),
};
} satisfies ExtractSize;
10 changes: 10 additions & 0 deletions lib/formats/dds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractDdsSize = async function* (stream) {
if (await stream.readString(0, 4) !== 'DDS ')
return;
return yield {
height: await stream.readUint32LE(12),
width: await stream.readUint32LE(16),
};
} satisfies ExtractSize;
10 changes: 10 additions & 0 deletions lib/formats/gif.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractGifSize = async function* (stream) {
if (!/^GIF8[79]a$/.test(await stream.read(0, 6) + ''))
return;
return yield {
width: await stream.readUint16LE(6),
height: await stream.readUint16LE(8),
};
} satisfies ExtractSize;
62 changes: 62 additions & 0 deletions lib/formats/icns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractIcnsSize = async function* (stream) {
if (await stream.readString(0, 4) !== 'icns')
return;
const fileLength = await stream.readUint32BE(FILE_LENGTH_OFFSET);
let imageOffset = SIZE_HEADER;
do {
const iconType = await stream.readString(imageOffset, ENTRY_LENGTH_OFFSET);
const size = ICON_TYPE_SIZE[iconType];
if (!size)
throw new Error(`Unknown iconType: ${iconType}`);
yield { width: size, height: size, type: iconType };
imageOffset += await stream.readUint32BE(imageOffset + ENTRY_LENGTH_OFFSET);
} while (imageOffset !== fileLength);
} satisfies ExtractSize;

const SIZE_HEADER = 4 + 4;
const FILE_LENGTH_OFFSET = 4;
const ENTRY_LENGTH_OFFSET = 4;
const ICON_TYPE_SIZE: Record<string, number | undefined> = {
ICON: 32,
'ICN#': 32,
// m => 16 x 16
'icm#': 16,
icm4: 16,
icm8: 16,
// s => 16 x 16
'ics#': 16,
ics4: 16,
ics8: 16,
is32: 16,
s8mk: 16,
icp4: 16,
// l => 32 x 32
icl4: 32,
icl8: 32,
il32: 32,
l8mk: 32,
icp5: 32,
ic11: 32,
// h => 48 x 48
ich4: 48,
ich8: 48,
ih32: 48,
h8mk: 48,
// . => 64 x 64
icp6: 64,
ic12: 32,
// t => 128 x 128
it32: 128,
t8mk: 128,
ic07: 128,
// . => 256 x 256
ic08: 256,
ic13: 256,
// . => 512 x 512
ic09: 512,
ic14: 512,
// . => 1024 x 1024
ic10: 1024,
};
42 changes: 42 additions & 0 deletions lib/formats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

import { ExtractSize } from "../utils/sizeExtractor";
import {
extractCurSize,
extractIcoSize,
} from "./[ico,cur]";
import { extractBmpSize } from "./bmp";
import { extractDdsSize } from "./dds";
import { extractGifSize } from "./gif";
import { extractIcnsSize } from "./icns";
import { extractJ2cSize } from "./j2c";
import { extractJp2Size } from "./jp2";
import { extractJpgSize } from "./jpg";
import { extractKtxSize } from "./ktx";
import { extractPngSize } from "./png";
import { extractPnmSize } from "./pnm";
import { extractPsdSize } from "./psd";
import { extractSvgSize } from "./svg";
import { extractTgaSize } from "./tga";
import { extractTiffSize } from "./tiff";
import { extractWebpSize } from "./webp";

export const formats = <const>[
['ico', extractIcoSize],
['cur', extractCurSize],

['bmp', extractBmpSize],
['dds', extractDdsSize],
['gif', extractGifSize],
['icns', extractIcnsSize],
['j2c', extractJ2cSize],
['jp2', extractJp2Size],
['jpg', extractJpgSize],
['ktx', extractKtxSize],
['png', extractPngSize],
['pnm', extractPnmSize],
['psd', extractPsdSize],
['svg', extractSvgSize],
['tga', extractTgaSize],
['tiff', extractTiffSize],
['webp', extractWebpSize],
] satisfies ReadonlyArray<readonly [string, ExtractSize]>;
10 changes: 10 additions & 0 deletions lib/formats/j2c.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractJ2cSize = async function* (stream) {
if (await stream.readUInt32BE(0) !== 0xff4fff51)
return;
return yield {
width: await stream.readUint32BE(8),
height: await stream.readUint32BE(12),
};
} satisfies ExtractSize;
46 changes: 46 additions & 0 deletions lib/formats/jp2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ExtractSize } from "../utils/sizeExtractor";

export const extractJp2Size = async function* (stream) {
const signatureLength = await stream.readUInt32BE(0);
const signature = await stream.readString(4, 4);
if (signature !== BoxTypes.jp__ || signatureLength < 1)
return;
const ftypeBoxStart = signatureLength + 4;
const ftypBoxLength = await stream.readUInt32BE(signatureLength);
if (await stream.readString(ftypeBoxStart, 4) !== BoxTypes.ftyp)
return;
let ihdrOffset = signatureLength + 4 + ftypBoxLength;
switch (await stream.readString(ihdrOffset, 4)) {
default:
return;
case BoxTypes.rreq: {
ihdrOffset += 4;
const unit = await stream.readUint8(ihdrOffset);
let offset = 1 + 2 * unit;
const numStdFlags = await stream.readUInt16BE(ihdrOffset + offset);
const flagsLength = numStdFlags * (2 + unit);
offset += 2 + flagsLength;
const numVendorFeatures = await stream.readUInt16BE(ihdrOffset + offset);
const featuresLength = numVendorFeatures * (16 + unit);
const rreqLength = offset + 2 + featuresLength;
ihdrOffset += 4 + rreqLength;
ihdrOffset += 8;
} break;
case BoxTypes.jp2h: {
ihdrOffset += 8;
} break;
}
return yield {
height: await stream.readUint32BE(ihdrOffset + 4),
width: await stream.readUint32BE(ihdrOffset + 8),
};
} satisfies ExtractSize;

enum BoxTypes {
ftyp = 'ftyp',
ihdr = 'ihdr',
jp2h = 'jp2h',
jp__ = 'jP ',
rreq = 'rreq',
xml_ = 'xml ',
};
Loading

0 comments on commit a660ba4

Please sign in to comment.