diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..20a9307 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,43 @@ +name: docs + +# publishes the documentation to GitHub Pages (imagescript.matmen.dev). release.yml runs it after a release reached +# npm, so the site documents what is on npm; can also be run by hand from the Actions tab +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24.x + + - run: npm install + - run: npm run docs + + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 + with: + path: docs + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index dc9eab1..22ec9e9 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -1,25 +1,34 @@ name: node +# tests every pushed branch, and pull requests from forks (pull requests from branches of this repository are +# already tested by their push); release.yml runs it before publishing on: push: - branches: [dev, master] + branches: ['**'] pull_request: - branches: [dev, master] + workflow_call: + +# a newer push to the same branch cancels the tests of the previous one +concurrency: + group: node-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name != 'release' }} jobs: build: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ${{ matrix.os }} name: node ${{ matrix.version }} | ${{ matrix.os }} strategy: matrix: - version: [20.x, 22.x, 23.x, 24.x] + version: [22.13.0, 22.x, 24.x, 26.x] os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version: ${{ matrix.version }} + - run: npm install - run: npm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..50822b5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,85 @@ +name: release + +# publishing a GitHub release with the tag v publishes that version to npm and JSR: +# the release is checked (tag, semver, changelog, jsr.json, not yet on npm/JSR), tested on every supported platform, +# published to npm with provenance through trusted publishing and to JSR through GitHub OIDC (no tokens), +# then the docs are deployed +on: + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: release + cancel-in-progress: false + +jobs: + check: + runs-on: ubuntu-latest + outputs: + dist-tag: ${{ steps.check.outputs.dist-tag }} + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24.x + + - id: check + run: node scripts/release-check.js "$TAG" "$PRERELEASE" + env: + TAG: ${{ github.event.release.tag_name }} + PRERELEASE: ${{ github.event.release.prerelease }} + + test: + needs: check + uses: ./.github/workflows/node.yml + + publish: + needs: [check, test] + runs-on: ubuntu-latest + environment: + name: npm + url: https://www.npmjs.com/package/imagescript?activeTab=versions + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24.x + registry-url: https://registry.npmjs.org + + - run: npm install + # prepack builds dist; trusted publishing needs npm 11.5.1 or newer, which node 24 ships + - run: npm publish --provenance --access public --tag "$DIST_TAG" + env: + DIST_TAG: ${{ needs.check.outputs.dist-tag }} + + jsr: + needs: publish + runs-on: ubuntu-latest + environment: + name: jsr + url: https://jsr.io/@matmen/imagescript/versions + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v7 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + # publishes the TypeScript sources as they are, JSR authenticates the workflow through the linked repository + - run: deno publish + + docs: + needs: [check, publish] + if: needs.check.outputs.dist-tag == 'latest' + permissions: + contents: read + pages: write + id-token: write + uses: ./.github/workflows/docs.yml diff --git a/.gitignore b/.gitignore index a2e0e01..ea9737e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ old/ coverage/ .DS_Store .nyc_output/ -.node_modules/ -bench/node_modules/ -bench/package-lock.json \ No newline at end of file +node_modules/ +dist/ +docs/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 1f8796e..0000000 --- a/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea/ -tests/ -.github/ -coverage/ -.nyc_output/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..48c1f17 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,106 @@ +# Changelog + +## 3.0.0 + +ImageScript is now written in TypeScript and has no WebAssembly or native binaries anymore. Every codec and renderer +was rewritten, the npm package shrank from 3 MB to about 120 kB, and it installs and runs on any platform Node.js +supports (including Alpine/musl and bundled serverless functions). + +On JSR, `@matmen/imagescript` is now the same package as on npm (it used to be the Deno-only 1.x with WebAssembly), +published from the TypeScript sources with every release: `deno add jsr:@matmen/imagescript`. + +### Breaking changes + +- **Node.js 22.13 or newer is required.** The package is ES modules only; `require('imagescript')` keeps working + through Node's `require(esm)`. +- **All coordinates are 0-based.** `getPixelAt`, `setPixelAt`, `getRGBAAt`, iteration, `fill` callbacks, `drawBox` + and `drawCircle` used 1-based coordinates while `crop` and `composite` used 0-based offsets. Code that addressed + pixels starting at 1 now has to subtract 1; out of range coordinates throw. +- **WebP encoding and `encodeWEBP()` were removed**, use `encode()` (PNG) or `encodeJPEG()`. +- **`invertSaturation()` and `invertHue()` were removed.** `invertHue()` mirrored the hue (`h -> 1 - h`); for the + complementary color use `hueShift(180)`. +- **The experimental `imagescript/v2` API was removed.** +- **`TextLayout` alignment options are named correctly:** `horizontalAlign` takes `left`/`center`/`right`, + `verticalAlign` takes `top`/`middle`/`bottom`. Values passed under the old (swapped) names are still accepted. + Alignment and `wrapStyle: 'char'` actually work now, which changes the output of code that set them. +- **`GIF.encode(quality)`** scales the palette size: 1 = 8 colors, 50 = 55, 90 and above = 256. +- **`GIF.decode`** returns the GIF's real loop count (it always returned -1); GIFs without a loop extension play once (0). +- **Resizing defaults to bilinear filtering** (`Image.RESIZE_BILINEAR`) instead of nearest neighbor, which aliased + badly when shrinking photos. Pass `Image.RESIZE_NEAREST_NEIGHBOR` for pixel art; it now samples the pixel centers, + which shifts its output by up to a pixel. `RESIZE_AUTO` rounds the computed side instead of flooring it. +- **`drawCircle()`, `cropCircle()` and `roundCorners()` are antialiased.** `drawCircle(x, y, r)` is centered on the + center of pixel (x, y) and has a radius of exactly `r`. `cropCircle(max, feathering)`: `feathering` is now the share + of the radius (0..1) over which the edge fades out; it used to fade the whole circle. +- **`dominantColor()`** groups the colors into clusters (median cut and k-means) and returns the average color of the + largest one, instead of the most common bucket of a fine HSL histogram (which returned shifted colors, e.g. `#fb0000` + for pure red, and let small flat areas beat large varied ones). `bwThreshold` (still 0-64) now ignores colors whose + channels are all below / above the threshold, so dark saturated colors like navy are no longer discarded as black. +- **Text is laid out with subpixel precision and kerning** (GPOS and kern tables). Glyph advances used to be rounded + up to whole pixels, so text gets narrower and more evenly spaced. +- **`encode()`** writes the smallest lossless PNG color type: a palette for images with up to 256 colors, otherwise + greyscale or RGB, with an alpha channel only if needed. Rows of non-palette images are paeth filtered, which makes + photos about 25% and gradients up to 95% smaller. Compression uses the runtime's native zlib when available + (node:zlib), which is several times faster than the JS fallback. +- **JPEGs, TIFFs and PNGs are rotated/mirrored as their orientation tag says**, like browsers display them. Photos + taken with a rotated camera used to come out sideways. PNGs take it from the eXIf chunk, or without one from + ImageMagick's Exif text or the XMP packet (which browsers ignore). `Image.decode(data, {applyOrientation: false})` keeps the + stored pixels. +- **`rgbaToColor()` / `rgbToColor()` round and clamp the components** to 0..255. Out of range values used to wrap + around (256 became 0) and fractions were truncated. +- **`Frame.disposalMode` returns the mode's name** (`'keep'`, ...), matching the `Frame.DISPOSAL_*` constants and what + it is set to; it used to return the numeric id. +- **`invertValue()`** inverts the HSV value (the brightest channel `v` becomes `255 - v`, black becomes white). It used + to invert HSL lightness, which `invertLightness()` does now. +- **`rotate()`** is documented as counterclockwise, which it always was. +- **`averageColor()`** weights pixels by their opacity. + +### Fixes + +- `rotate(90)` / `rotate(270)` scrambled non-square images (#29); free rotation lost the image edges +- `composite()` darkened semi-transparent pixels drawn onto transparent ones +- `crop()` read pixels from the next row when the region exceeded the image +- `drawBox()` threw when the box was outside the image +- `cropCircle(max)` ignored `max` +- `roundCorners()` was asymmetric and did nothing to the right/bottom corners for fractional radii +- `Frame.resize()` returned a frame without its duration, offsets and disposal mode; `Frame.clone()` returned an Image +- `GIF.slice()` without an end, `GIF.map()` and `GIF.filter()` threw +- `GIF.resize()` stretched partial frames to the full size +- `encode({...metadata})` ignored the metadata +- `fisheye()` shifted the image by a pixel +- HSL conversions truncated instead of rounding, so `lightness(1)`, `saturation(1)` or `hueShift(360)` changed the + image and repeated adjustments drifted darker; negative hues (e.g. `hueShift(-300)`) produced wrong colors +- `cover()` could return an image a pixel smaller than requested, `contain()` one a pixel smaller than the box +- Big endian TIFFs weren't recognized, and most TIFFs failed to decode +- Text alignment was ignored (#37) +- Assigning `image.bitmap` left `getPixelAt()` and other methods reading the old pixels +- GIF frame durations were truncated to centiseconds (16 ms became 10 ms), they are rounded now +- `invert()`, `dominantColor()`, `rotate()`, `hueShift()`, `lightness()`, `saturation()` and the other HSL based + functions are several times faster; PNG decoding uses native zlib when available + +### New + +- `blur(radius)` (gaussian, radius = standard deviation like CSS' `blur()`) and `sharpen(amount, radius)` (unsharp mask) +- `brightness(factor)`, `contrast(factor)`, `gamma(gamma)`, `greyscale()` (ITU-R 601 luma, like Pillow) and `sepia()` +- `composite(source, x, y, {opacity, mode})` with the blend modes of CSS' `mix-blend-mode` (`multiply`, `screen`, + `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, `exclusion`) + and `add` +- Outlined text: `renderText(font, size, text, color, layout, {strokeWidth, strokeColor})` +- `invertLightness()`, which does what `invertValue()` used to do (invert HSL lightness) +- `encodePNG()`, an alias of `encode()` +- Options objects as an alternative to positional arguments: `cropCircle({max, feathering})`, + `dominantColor({ignoreBlack, ignoreWhite, threshold})`, `rotate(angle, {resize})` +- `Frame.DISPOSAL_ANY`; `ImageType.isPNG()` and the other checks accept any binary data, not only a `DataView` +- Parameter names describe what they are: `red/green/blue(factor)`, `renderText(font, size, ...)`, `fisheye(strength)` +- Resize modes `Image.RESIZE_BILINEAR` (default), `Image.RESIZE_BICUBIC` and `Image.RESIZE_AREA`, all usable with + `resize`, `scale`, `contain`, `fit`, `cover` and `GIF.resize` + +### Codecs + +- PNG: all color types and bit depths, interlacing, tRNS transparency, checksum validation; inflating uses the + runtime's native zlib when available +- JPEG: baseline and progressive decoding (bit-exact with libjpeg-turbo), encoding with optimized Huffman tables +- GIF: encoding with a median cut quantizer and dithering, tolerant decoding +- TIFF: strips and tiles, LZW/deflate/PackBits/JPEG compression, 1-32 bit and float samples, CMYK and YCbCr, BigTIFF +- Fonts: TrueType and OpenType (CFF) outlines; much faster for large fonts +- SVG: shapes, paths, transforms, use/symbol, CSS, gradients, strokes, clip paths and masks. Text, images, filters + and patterns are not rendered. diff --git a/ImageScript.d.ts b/ImageScript.d.ts deleted file mode 100644 index dda0c78..0000000 --- a/ImageScript.d.ts +++ /dev/null @@ -1,954 +0,0 @@ -export class Image { - private __width__: number; - private __height__: number; - private __buffer__: ArrayBuffer; - private __view__: DataView; - private __u32__: Uint32Array; - bitmap: Uint8ClampedArray; - - constructor(width: number, height: number); - - private toString(): `Image<${number}x${number}>`; - - get width(): number; - - get height(): number; - - *[Symbol.iterator](): void; - - *iterateWithColors(): Generator< - [x: number, y: number, color: number], - void, - unknown ->; - static rgbaToColor(r: number, g: number, b: number, a: number): number; - - static rgbToColor(r: number, g: number, b: number): number; - - static hslaToColor(h: number, s: number, l: number, a: number): number; - - static hslToColor(h: number, s: number, l: number): number; - - static rgbaToHSLA(r: number, g: number, b: number, a: number): number[]; - - static colorToRGBA(color: number): number[]; - - static colorToRGB(color: number): number[]; - - getPixelAt(x: number, y: number): number; - - getRGBAAt(x: number, y: number): Uint8ClampedArray; - - setPixelAt(x: number, y: number, pixelColor: number): this; - - private __set_pixel__(x: number, y: number, pixelColor: number): void; - - private __check_boundaries__(x: number, y: number): void; - - private static get __out_of_bounds__(): string; - - /** - * Fills the entire image with the supplied color. - * - * @param color - */ - fill(color: number | ColorFunction): this; - - clone(): Image; - - /** - * Use - * {@link https://en.wikipedia.org/wiki/Image_scaling#Nearest-neighbor_interpolation Nearest-neighbor} - * resizing. - */ - static get RESIZE_NEAREST_NEIGHBOR(): "RESIZE_NEAREST_NEIGHBOR"; - - /** - * Used for automatically preserving an image's aspect ratio when resizing. - */ - static get RESIZE_AUTO(): -1; - - /** - * Resizes the image by the given factor. - * - * @param factor Fraction, where: - * - `0.5` is "50%" (half) - * - `1.0` is "100%" (same size) - * - `2.0` is "200%" (double) - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - scale(factor: number, mode?: ResizeMode): this; - - private __scale__(factor: number, mode?: ResizeMode); - - /** - * Resizes the image to the given dimensions. - * Use {@link Image.RESIZE_AUTO} as either width or height to automatically - * preserve the aspect ratio. - * - * @param width The new width. - * @param height The new height. - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - resize(width: number, height: number, mode?: ResizeMode): this; - - /** - * Resizes the image so it is contained in the given bounding box. - * Can return an image with one axis smaller than the given bounding box. - * - * @param width The width of the bounding box - * @param height The height of the bounding box - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - contain(width: number, height: number, mode?: ResizeMode): this; - - /** - * Resizes the image so it is contained in the given bounding box, placing it in the center of the given bounding box. - * Always returns the exact dimensions of the bounding box. - * - * @param width The width of the bounding box - * @param height The height of the bounding box - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - fit(width: number, height: number, mode?: ResizeMode): this; - - /** - * Resizes the image so it covers the given bounding box, cropping the overflowing edges. - * Always returns the exact dimensions of the bounding box. - * - * @param width The width of the bounding box - * @param height The height of the bounding box - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - cover(width: number, height: number, mode?: ResizeMode): this; - - private __resize__(width: number, height: number, mode?: ResizeMode): this; - - private __resize_nearest_neighbor__(width: number, height: number): this; - - crop(x: number, y: number, width: number, height: number): this; - - private __crop__(x: number, y: number, width: number, height: number): this; - - /** - * Draws a box at the specified coordinates. - */ - drawBox( - x: number, - y: number, - width: number, - height: number, - color: number | ColorFunction - ): this; - - private __fast_box__( - x: number, - y: number, - width: number, - height: number, - color: number - ): this; - - /** - * Draws a circle at the specified coordinates with the specified radius. - */ - drawCircle( - x: number, - y: number, - radius: number, - color: number | ColorFunction - ): this; - - /** - * Crops the image into a circle. - * - * @param max Whether to use the larger dimension for the size. Default: `false` - * @param feathering How much feathering to apply to the edges. Default: `0` - */ - cropCircle(max?: boolean, feathering?: number): this; - - /** - * Sets the image's opacity. - * - * @param opacity `0`-`1`, where `0` is completely transparent and - * `1` is completely opaque. - * @param absolute Whether to scale the current opacity (`false`) or - * just set the new opacity (`true`). Default: `false` - */ - opacity(opacity: number, absolute?: boolean): this; - - /** - * Set the red channel's saturation value. - * - * @param saturation `0`-`1` - * @param absolute Whether to scale the current saturation (`false`) or - * just set the new saturation (`true`). Default: `false` - */ - red(saturation: number, absolute?: boolean): this; - - /** - * Set the green channel's saturation value. - * - * @param saturation `0`-`1` - * @param absolute Whether to scale the current saturation (`false`) or - * just set the new saturation (`true`). Default: `false` - */ - green(saturation: number, absolute?: boolean): this; - - /** - * Set the blue channel's saturation value. - * - * @param saturation `0`-`1` - * @param absolute Whether to scale the current saturation (`false`) or - * just set the new saturation (`true`). Default: `false` - */ - blue(saturation: number, absolute?: boolean): this; - - private __set_channel_value__( - value: number, - absolute: boolean, - offset: number - ): void; - - /** - * Sets the brightness of the image. - * - * @param value `0`-`1` - * @param absolute Whether to scale the current lightness (`false`) or - * just set the new lightness (`true`). Default: `false` - */ - lightness(value: number, absolute?: boolean): this; - - /** - * Sets the saturation of the image. - * - * @param value `0`-`1` - * @param absolute Whether to scale the current saturation (`false`) or - * just set the new saturation (`true`). Default: `false` - */ - saturation(value: number, absolute?: boolean): this; - - /** - * Composites (overlays) the {@link source} onto this image at the - * specified coordinates. - */ - composite(source: this, x?: number, y?: number): this; - - /** - * Inverts the image's colors. - */ - invert(): this; - - /** - * Inverts the image's value (lightness). - */ - invertValue(): this; - - /** - * Inverts the image's saturation. - */ - invertSaturation(): this; - - /** - * Inverts the image's hue. - */ - invertHue(): this; - - /** - * Shifts the image's hue. - */ - hueShift(degrees: number): this; - - /** - * Gets the average color of the image. - */ - averageColor(): number; - - /** - * Gets the image's dominant color. - * - * @param ignoreBlack Whether to ignore dark colors below the threshold. - * Default: `true` - * @param ignoreWhite Whether to ignore light colors above the threshold. - * Default: `true` - * @param bwThreshold The black/white threshold (`0`-`64`). - * Default: `0xf` (`15`) - */ - dominantColor( - ignoreBlack?: boolean, - ignoreWhite?: boolean, - bwThreshold?: number - ): number; - - /** - * Rotates the image the given amount of degrees. - * - * @param angle The angle to rotate the image for (in degrees) - * @param resize Whether to resize the image so it fits all pixels (`true`) or - * just ignore outlying pixels (`false`). Default: `true` - */ - rotate(angle: number, resize?: boolean): this; - - /** - * Flips / mirrors the image horizontally or vertically. - */ - flip(direction: "horizontal" | "vertical"): this; - - private __apply__(image: this | Frame): this | Frame; - - /** - * Creates a multi-point gradient generator. - * - * @param colors The gradient points to use - * (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`). - * @returns The gradient generator. The function argument is the position - * in the gradient (`0`-`1`). - */ - static gradient(colors: { - [position: number]: number; - }): (position: number) => number; - - /** - * Rounds the image's corners. - * - * @param radius Default: `min(width, height) / 4` - */ - roundCorners(radius?: number): this; - - private static __gradient__(startColor: number, endColor: number): number; - - /** - * @param radius Default: `2` - */ - fisheye(radius?: number): this; - - /** - * Encodes the image into a PNG. - * - * @param compression `0`-`9`, where `0` is no compression and `9` is highest - * compression (default: `1`) - * @param metadata - */ - async encode( - compression?: PNGCompressionLevel, - metadata?: PNGMetadata - ): Promise; - async encode(metadata?: PNGMetadata): Promise; - - /** - * Encodes the image into a JPEG. - * - * @param quality `1`-`100`, where `1` is lowest quality (highest compression) - * and `100` is highest quality (lowest compression). Default: `90` - */ - async encodeJPEG(quality?: JPEGQuality): Promise; - - /** - * Encodes the image into a WEBP. - * - * @param quality `0`-`100`, or `null` for lossless. `0` is lowest quality - * (highest compression) and `100` is highest quality (lowest compression). - * Default: `null` - */ - async encodeWEBP(quality?: null | WEBPQuality): Promise; - - /** - * Decodes an image (PNG, JPEG or TIFF). - * - * @param data The binary data to decode - * @returns The decoded image - */ - static async decode(data: Buffer | Uint8Array): Promise; - - /** - * Scale the SVG by the given amount. For use with {@link Image.renderSVG}. - */ - static get SVG_MODE_SCALE(): 1; - - /** - * Scale the SVG to fit the given width. For use with {@link Image.renderSVG}. - */ - static get SVG_MODE_WIDTH(): 2; - - /** - * Scale the SVG to fit the given height. For use with {@link Image.renderSVG}. - */ - static get SVG_MODE_HEIGHT(): 3; - - /** - * Creates a new image from the given SVG. - * - * @param svg - * @param size - * @param mode {@link Image.SVG_MODE_SCALE}, {@link Image.SVG_MODE_WIDTH}, or - * {@link Image.SVG_MODE_HEIGHT}. - * - * @returns New bitmap image with the rendered {@link svg}. - */ - static async renderSVG( - svg: string, - size?: number, - mode?: SVGScaleMode - ): Promise; - - /** - * Creates a new image containing the rendered text. - * - * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use. - * @param scale - * @param text - * @param color - * @param layout - * - * @returns New image with the rendered {@link text}. - */ - static async renderText( - font: Uint8Array, - scale: number, - text: string, - color?: number, - layout?: TextLayout - ): Promise; -} - -export type FrameDisposalModeName = "any" | "keep" | "previous" | "background"; - -export type FrameDisposalModeId = 0 | 1 | 2 | 3; - -/** - * Represents a frame in a GIF. - */ -export class Frame extends Image { - static get DISPOSAL_KEEP(): "keep"; - - static get DISPOSAL_PREVIOUS(): "previous"; - - static get DISPOSAL_BACKGROUND(): "background"; - - private static __convert_disposal_mode__( - mode: FrameDisposalModeName | FrameDisposalModeId - ): FrameDisposalModeId; - - /** - * Creates a new, blank frame. - * - * @param width - * @param height - * @param duration Milliseconds (default: `100`) - * @param xOffset Offset on the X-axis (default: `0`) - * @param yOffset Offset on the y-axis (default: `0`) - * @param disposalMode The frame's disposal mode (default: `'keep'`) - */ - constructor( - width: number, - height: number, - duration: number, - xOffset?: number, - yOffset?: number, - disposalMode?: FrameDisposalModeName | FrameDisposalModeId - ); - - /** - * Milliseconds. - */ - duration: number; - - xOffset: number; - - yOffset: number; - - get disposalMode(): FrameDisposalModeId; - - set disposalMode(disposalMode: FrameDisposalModeName | FrameDisposalModeId); - - toString(): `Frame<${number}x${number}x${number}ms>`; - - /** - * Converts an Image instance to a Frame, cloning it in the process - * @param image The image to create the frame from - * @param duration Milliseconds (default: `100`) - * @param xOffset Offset on the X-axis (default: `0`) - * @param yOffset Offset on the y-axis (default: `0`) - * @param disposalMode The frame's disposal mode (default: `'keep'`) - */ - static from( - image: Image, - duration?: number, - xOffset?: number, - yOffset?: number, - disposalMode?: FrameDisposalModeName | FrameDisposalModeId - ): Frame; - - /** - * @param width - * @param height - * @param mode Default: {@link Frame.DISPOSAL_KEEP} - */ - resize( - width: number, - height: number, - mode?: typeof Image.RESIZE_NEAREST_NEIGHBOR | string - ): Image; -} - -/** - * Represents a GIF image as an array of frames. - */ -export class GIF extends Array { - /** - * @param frames - * @param loopCount How many times to loop the GIF for (`-1` = unlimited). - */ - constructor(frames: Frame[], loopCount?: number); - - get width(): number; - - get height(): number; - - toString(): `GIF<${number}x${number}x${number}ms>`; - - *[Symbol.iterator](): Generator; - - slice(start: number, end: number): GIF; - - /** - * Milliseconds. - */ - get duration(): number; - - /** - * @param quality GIF quality `0`-`100` (default: `95`) - */ - async encode(quality?: GIFQuality): Promise; - - /** - * @param data - * @param onlyExtractFirstFrame Whether to end GIF decoding after the first - * frame (default: `false`) - */ - static async decode( - data: Buffer | Uint8Array, - onlyExtractFirstFrame?: boolean - ): Promise; - - /** - * @param width - * @param height - * @param mode Default: {@link Image.RESIZE_NEAREST_NEIGHBOR} - */ - resize(width: number, height: number, mode?: ResizeMode): void; -} - -export type WrapStyle = "word" | "char"; - -export type VerticalAlign = "left" | "center" | "right"; - -export type HorizontalAlign = "top" | "middle" | "bottom"; - -export class TextLayout { - /** - * @param options Defaults: - * ```js - * { - * maxWidth: Infinity, - * maxHeight: Infinity, - * wrapStyle: 'word', - * verticalAlign: 'left', - * horizontalAlign: 'top', - * wrapHardBreaks: true, - * } - * ``` - */ - constructor(options?: { - /** @default Infinity */ - maxWidth?: number; - - /** @default Infinity */ - maxHeight?: number; - - /** @default 'word' */ - wrapStyle?: WrapStyle; - - /** @default 'left' */ - verticalAlign?: VerticalAlign; - - /** @default 'top' */ - horizontalAlign?: HorizontalAlign; - - /** @default true */ - wrapHardBreaks?: boolean; - }); -} - -export type ImageTypeName = "png" | "jpeg" | "tiff" | "gif"; - -export class ImageType { - static getType(data: Buffer | Uint8Array): ImageTypeName | null; - - static isPNG(view: DataView): boolean; - - static isJPEG(view: DataView): boolean; - - static isTIFF(view: DataView): boolean; - - static isGIF(view: DataView): boolean; -} - -/** - * @param data - * @param onlyExtractFirstFrame Whether to end GIF decoding after the first - * frame (default: `false`) - */ -export function decode( - data: Uint8Array | Buffer, - onlyExtractFirstFrame?: boolean -): Promise; - -export type PNGMetadata = { - title?: string; - author?: string; - description?: string; - copyright?: string; - creationTime?: string | number | Date; - software?: string; - disclaimer?: string; - warning?: string; - source?: string; - comment?: string; -}; - -export type ColorFunction = (x: number, y: number) => number; - -export type ResizeMode = "RESIZE_NEAREST_NEIGHBOR" | -1; - -/** - * - `0` = no compression - * - `9` = highest compression - */ -export type PNGCompressionLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; - -/** - * {@link Image.SVG_MODE_SCALE}, {@link Image.SVG_MODE_WIDTH}, or - * {@link Image.SVG_MODE_HEIGHT}. - */ -export type SVGScaleMode = 1 | 2 | 3; - -/** - * - `0` = **lowest** quality (smallest file size) - * - `100` = **highest** quality (largest file size) - */ -export type WEBPQuality = - | 0 - | 1 - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 13 - | 14 - | 15 - | 16 - | 17 - | 18 - | 19 - | 20 - | 21 - | 22 - | 23 - | 24 - | 25 - | 26 - | 27 - | 28 - | 29 - | 30 - | 31 - | 32 - | 33 - | 34 - | 35 - | 36 - | 37 - | 38 - | 39 - | 40 - | 41 - | 42 - | 43 - | 44 - | 45 - | 46 - | 47 - | 48 - | 49 - | 50 - | 51 - | 52 - | 53 - | 54 - | 55 - | 56 - | 57 - | 58 - | 59 - | 60 - | 61 - | 62 - | 63 - | 64 - | 65 - | 66 - | 67 - | 68 - | 69 - | 70 - | 71 - | 72 - | 73 - | 74 - | 75 - | 76 - | 77 - | 78 - | 79 - | 80 - | 81 - | 82 - | 83 - | 84 - | 85 - | 86 - | 87 - | 88 - | 89 - | 90 - | 91 - | 92 - | 93 - | 94 - | 95 - | 96 - | 97 - | 98 - | 99 - | 100; - -/** - * - `0` = **lowest** quality (smallest file size) - * - `100` = **highest** quality (largest file size) - */ -export type JPEGQuality = - | 1 - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 13 - | 14 - | 15 - | 16 - | 17 - | 18 - | 19 - | 20 - | 21 - | 22 - | 23 - | 24 - | 25 - | 26 - | 27 - | 28 - | 29 - | 30 - | 31 - | 32 - | 33 - | 34 - | 35 - | 36 - | 37 - | 38 - | 39 - | 40 - | 41 - | 42 - | 43 - | 44 - | 45 - | 46 - | 47 - | 48 - | 49 - | 50 - | 51 - | 52 - | 53 - | 54 - | 55 - | 56 - | 57 - | 58 - | 59 - | 60 - | 61 - | 62 - | 63 - | 64 - | 65 - | 66 - | 67 - | 68 - | 69 - | 70 - | 71 - | 72 - | 73 - | 74 - | 75 - | 76 - | 77 - | 78 - | 79 - | 80 - | 81 - | 82 - | 83 - | 84 - | 85 - | 86 - | 87 - | 88 - | 89 - | 90 - | 91 - | 92 - | 93 - | 94 - | 95 - | 96 - | 97 - | 98 - | 99 - | 100; - -/** - * - `0` = **lowest** quality (smallest file size) - * - `100` = **highest** quality (largest file size) - */ -export type GIFQuality = - | 0 - | 1 - | 2 - | 3 - | 4 - | 5 - | 6 - | 7 - | 8 - | 9 - | 10 - | 11 - | 12 - | 13 - | 14 - | 15 - | 16 - | 17 - | 18 - | 19 - | 20 - | 21 - | 22 - | 23 - | 24 - | 25 - | 26 - | 27 - | 28 - | 29 - | 30 - | 31 - | 32 - | 33 - | 34 - | 35 - | 36 - | 37 - | 38 - | 39 - | 40 - | 41 - | 42 - | 43 - | 44 - | 45 - | 46 - | 47 - | 48 - | 49 - | 50 - | 51 - | 52 - | 53 - | 54 - | 55 - | 56 - | 57 - | 58 - | 59 - | 60 - | 61 - | 62 - | 63 - | 64 - | 65 - | 66 - | 67 - | 68 - | 69 - | 70 - | 71 - | 72 - | 73 - | 74 - | 75 - | 76 - | 77 - | 78 - | 79 - | 80 - | 81 - | 82 - | 83 - | 84 - | 85 - | 86 - | 87 - | 88 - | 89 - | 90 - | 91 - | 92 - | 93 - | 94 - | 95 - | 96 - | 97 - | 98 - | 99 - | 100; diff --git a/ImageScript.js b/ImageScript.js deleted file mode 100644 index 62197e5..0000000 --- a/ImageScript.js +++ /dev/null @@ -1,1669 +0,0 @@ -const png = require('./png/node.js'); -const mem = require('./utils/mem.js'); -const {version} = require('./package.json'); -const codecs = require('./codecs/node/index.js'); -const { default: v2 } = require('./v2/framebuffer.js'); - -// old -const svglib = require('./wasm/node/svg.js'); -const giflib = require('./wasm/node/gif.js'); -const pnglib = require('./wasm/node/png.js'); -const fontlib = require('./wasm/node/font.js'); -const jpeglib = require('./wasm/node/jpeg.js'); -const tifflib = require('./wasm/node/tiff.js'); - -const MAGIC_NUMBERS = { - PNG: 0x89504e47, - JPEG: 0xffd8ff, - TIFF: 0x49492a00, - GIF: 0x474946 -}; - -/** - * Represents an image; provides utility functions - */ -class Image { - /** - * Creates a new image with the given dimensions - * @param {number} width - * @param {number} height - * @returns {Image} - */ - constructor(width, height) { - width = ~~width; - height = ~~height; - - if (width < 1) - throw new RangeError('Image has to be at least 1 pixel wide'); - if (height < 1) - throw new RangeError('Image has to be at least 1 pixel high'); - - /** @private */ - this.__width__ = width; - /** @private */ - this.__height__ = height; - /** @private */ - this.__buffer__ = new ArrayBuffer(width * height * 4); - /** @private */ - this.__view__ = new DataView(this.__buffer__); - /** @private */ - this.__u32__ = new Uint32Array(this.__buffer__); - /** - * The images RGBA pixel data - * @type {Uint8ClampedArray} - */ - this.bitmap = new Uint8ClampedArray(this.__buffer__); - } - - /** - * @private - * @returns {string} - */ - toString() { - return `Image<${this.width}x${this.height}>`; - } - - /** - * The images width - * @returns {number} - */ - get width() { - return this.__width__; - } - - /** - * The images height - * @returns {number} - */ - get height() { - return this.__height__; - } - - /** - * Yields an [x, y] array for every pixel in the image - * @yields {number[]} The coordinates of the pixel ([x, y]) - * @returns {void} - */ - *[Symbol.iterator]() { - for (const x of new v2(this.width, this.height, this.bitmap)[Symbol.iterator]()) yield (x[0]++, x[1]++, x) - } - - /** - * Yields an [x, y, color] array for every pixel in the image - * @yields {number[]} The coordinates and color of the pixel ([x, y, color]) - */ - *iterateWithColors() { - for (const x of new v2(this.width, this.height, this.bitmap).pixels('int')) yield (x[0]++, x[1]++, x); - } - - /** - * Converts RGBA components to an RGBA value - * @param {number} r red (0..255) - * @param {number} g green (0..255) - * @param {number} b blue (0..255) - * @param {number} a alpha (0..255) - * @returns {number} RGBA value - */ - static rgbaToColor(r, g, b, a) { - return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0; - } - - /** - * Converts RGB components to an RGBA value (assuming alpha = 255) - * @param {number} r red (0..255) - * @param {number} g green (0..255) - * @param {number} b blue (0..255) - * @returns {number} RGBA value - */ - static rgbToColor(r, g, b) { - return Image.rgbaToColor(r, g, b, 0xff); - } - - /** - * Converts HSLA colors to RGBA colors - * @param {number} h hue (0..1) - * @param {number} s saturation (0..1) - * @param {number} l lightness (0..1) - * @param {number} a opacity (0..1) - * @returns {number} color - */ - static hslaToColor(h, s, l, a) { - h %= 1; - s = Math.min(1, Math.max(0, s)); - l = Math.min(1, Math.max(0, l)); - a = Math.min(1, Math.max(0, a)); - - let r, g, b; - - if (s === 0) { - r = g = b = l; - } else { - const hue2rgb = (p, q, t) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - }; - - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - - r = hue2rgb(p, q, h + 1 / 3); - g = hue2rgb(p, q, h); - b = hue2rgb(p, q, h - 1 / 3); - } - - return Image.rgbaToColor(r * 255, g * 255, b * 255, a * 255); - } - - /** - * Converts HSL colors to RGBA colors (assuming an opacity of 255) - * @param {number} h hue (0..1) - * @param {number} s saturation (0..1) - * @param {number} l lightness (0..1) - * @returns {number} color - */ - static hslToColor(h, s, l) { - return Image.hslaToColor(h, s, l, 1); - } - - /** - * Converts an RGBA value to an array of HSLA values - * @param r {number} (0..255) - * @param g {number} (0..255) - * @param b {number} (0..255) - * @param a {number} (0..255) - * @returns {number[]} The HSLA values ([H, S, L, A]) - */ - static rgbaToHSLA(r, g, b, a) { - r /= 255; - g /= 255; - b /= 255; - - const max = Math.max(r, g, b), min = Math.min(r, g, b); - let h, s, l = (max + min) / 2; - - if (max === min) { - h = s = 0; - } else { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - } - - h /= 6; - } - - return [h, s, l, a / 255]; - } - - /** - * Converts a color value to an array of RGBA values - * @param {number} color The color value to convert - * @returns {number[]} The RGBA values ([R, G, B, A]) - */ - static colorToRGBA(color) { - return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff]; - } - - /** - * Converts a color value to an array of RGB values (ignoring the colors alpha) - * @param {number} color The color value to convert - * @returns {number[]} The RGB values ([R, G, B]) - */ - static colorToRGB(color) { - return Image.colorToRGBA(color).slice(0, 3); - } - - /** - * Gets the pixel color at the specified position - * @param {number} x - * @param {number} y - * @returns {number} The color value - */ - getPixelAt(x, y) { - this.__check_boundaries__(x, y); - return this.__view__.getUint32(((~~y - 1) * this.width + (~~x - 1)) * 4, false); - } - - /** - * Gets the pixel color at the specified position - * @param {number} x - * @param {number} y - * @returns {Uint8ClampedArray} The RGBA value - */ - getRGBAAt(x, y) { - this.__check_boundaries__(x, y); - const idx = ((~~y - 1) * this.width + (~~x - 1)) * 4; - return this.bitmap.subarray(idx, idx + 4); - } - - /** - * Sets the pixel color for the specified position - * @param {number} x - * @param {number} y - * @param {number} pixelColor - */ - setPixelAt(x, y, pixelColor) { - x = ~~x; - y = ~~y; - this.__check_boundaries__(x, y); - this.__set_pixel__(x, y, pixelColor); - return this; - } - - /** - * @private - * @param {number} x - * @param {number} y - * @param {number} pixelColor - */ - __set_pixel__(x, y, pixelColor) { - this.__view__.setUint32(((y - 1) * this.width + (x - 1)) * 4, pixelColor, false); - } - - /** - * @private - * @param {number} x - * @param {number} y - */ - __check_boundaries__(x, y) { - if (isNaN(x)) throw new TypeError(`Invalid pixel coordinates (x=${x})`); - if (isNaN(y)) throw new TypeError(`Invalid pixel coordinates (y=${y})`); - if (x < 1) - throw new RangeError(`${Image.__out_of_bounds__} (x=${x})<1`); - if (x > this.width) - throw new RangeError(`${Image.__out_of_bounds__} (x=${x})>(width=${this.width})`); - if (y < 1) - throw new RangeError(`${Image.__out_of_bounds__} (y=${y})<1`); - if (y > this.height) - throw new RangeError(`${Image.__out_of_bounds__} (y=${y})>(height=${this.height})`); - } - - /** - * @private - */ - static get __out_of_bounds__() { - return 'Tried referencing a pixel outside of the images boundaries:'; - } - - /** - * @callback colorFunction - * @param {number} x - * @param {number} y - * @returns {number} pixel color - */ - - /** - * Fills the image data with the supplied color - * @param {number|colorFunction} color - * @returns {Image} - */ - fill(color) { - new v2(this.width, this.height, this.bitmap).fill(color); - - return this; - } - - /** - * Clones the current image - * @returns {Image} - */ - clone() { - const image = new Image(this.width, this.height); - image.bitmap.set(this.bitmap); - return image; - } - - /** - * Use {@link https://en.wikipedia.org/wiki/Image_scaling#Nearest-neighbor_interpolation Nearest-neighbor} resizing. - * @returns {string} - */ - static get RESIZE_NEAREST_NEIGHBOR() { - return 'RESIZE_NEAREST_NEIGHBOR'; - } - - /** - * Used for automatically preserving an images aspect ratio when resizing. - * @returns {number} - */ - static get RESIZE_AUTO() { - return -1; - } - - /** - * Resizes the image by the given factor - * @param {number} factor The factor to resize the image with - * @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use - * @returns {Image} - */ - scale(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const image = this.__scale__(factor, mode); - return this.__apply__(image); - } - - /** @private */ - __scale__(factor, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - if (factor === 1) return this; - return this.__resize__(this.width * factor, this.height * factor, mode); - } - - /** - * Resizes the image to the given dimensions. - * Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio. - * @param {number} width The new width - * @param {number} height The new height - * @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use - * @returns {Image} The resized image - */ - resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const image = this.__resize__(width, height, mode); - return this.__apply__(image); - } - - /** - * Resizes the image so it is contained in the given bounding box. - * Can return an image with one axis smaller than the given bounding box. - * @param {number} width The width of the bounding box - * @param {number} height The height of the bounding box - * @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use - * @returns {Image} The resized image - */ - contain(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const scaleFactor = width / height > this.width / this.height ? height / this.height : width / this.width; - return this.scale(scaleFactor, mode); - } - - /** - * Resizes the image so it is contained in the given bounding box, placing it in the center of the given bounding box. - * Always returns the exact dimensions of the bounding box. - * @param {number} width The width of the bounding box - * @param {number} height The height of the bounding box - * @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use - * @returns {Image} The resized image - */ - fit(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const result = new Image(width, height); - this.contain(width, height, mode); - result.composite(this, (width - this.width) / 2, (height - this.height) / 2); - return this.__apply__(result); - } - - /** - * Resizes the image so it covers the given bounding box, cropping the overflowing edges. - * Always returns the exact dimensions of the bounding box. - * @param {number} width The width of the bounding box - * @param {number} height The height of the bounding box - * @param {string} [mode=Image.RESIZE_NEAREST_NEIGHBOR] The resizing mode to use - * @returns {Image} The resized image - */ - cover(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const scaleFactor = width / height > this.width / this.height ? width / this.width : height / this.height; - const result = this.scale(scaleFactor, mode); - return result.crop((result.width - width) / 2, (result.height - height) / 2, width, height); - } - - /** @private */ - __resize__(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO) throw new Error('RESIZE_AUTO can only be used for either width or height, not for both'); - else if (width === Image.RESIZE_AUTO) width = this.width / this.height * height; - else if (height === Image.RESIZE_AUTO) height = this.height / this.width * width; - - width = Math.floor(width); - height = Math.floor(height); - if (width < 1) - throw new RangeError('Image has to be at least 1 pixel wide'); - if (height < 1) - throw new RangeError('Image has to be at least 1 pixel high'); - - let image; - if (mode === Image.RESIZE_NEAREST_NEIGHBOR) - image = this.__resize_nearest_neighbor__(width, height); - else throw new Error('Invalid resize mode'); - - return image; - } - - /** - * @private - * @param {number} width The new width - * @param {number} height The new height - */ - __resize_nearest_neighbor__(width, height) { - const image = new this.constructor(width, height); - const frame = new v2(this.width, this.height, this.bitmap).resize('nearest', width, height); - - image.bitmap.set(frame.u8); - - return image; - } - - /** - * Crops an image to the specified dimensions - * @param {number} x The x offset - * @param {number} y The y offset - * @param {number} width The new images width - * @param {number} height The new images height - * @returns {Image} - */ - crop(x, y, width, height) { - if (width > this.width) width = this.width; - if (height > this.height) height = this.height; - - return this.__apply__(this.__crop__(~~x, ~~y, ~~width, ~~height)); - } - - /** - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - * @returns {Image} - * @private - */ - __crop__(x, y, width, height) { - x = ~~x; - y = ~~y; - - const image = new this.constructor(width, height); - - for (let tY = 0; tY < height; tY++) { - const idx = (tY + y) * this.width + x; - image.__u32__.set(this.__u32__.subarray(idx, idx + width), tY * width); - } - - return image; - } - - /** - * Draws a box at the specified coordinates - * @param {number} x The x offset - * @param {number} y The y offset - * @param {number} width The box width - * @param {number} height The box height - * @param {number|colorFunction} color The color to fill the box in with - * @returns {Image} - */ - drawBox(x, y, width, height, color) { - x = ~~(x - 1); - y = ~~(y - 1); - width = ~~width; - height = ~~height; - - if (typeof color === 'function') { - for (let tY = 1; tY <= height; tY++) { - for (let tX = 1; tX <= width; tX++) { - const nX = tX + x; - const nY = tY + y; - if (Math.min(nX, nY) < 1 || nX > this.width || nY > this.height) - continue; - - const tC = color(tX, tY); - this.__set_pixel__(nX, nY, tC); - } - } - } else return this.__fast_box__(x, y, width, height, color); - - return this; - } - - /** - * @private - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - * @param {number} color - */ - __fast_box__(x, y, width, height, color) { - if (x < 0) { - width += x; - x = 0; - } - - if (y < 0) { - height += y; - y = 0; - } - - const right = Math.max(Math.min(x + width, this.width), 1); - let xPos = right; - while (x <= --xPos) - this.__view__.setUint32(4 * (xPos + y * this.width), color); - const end = 4 * (right + y * this.width); - const start = 4 * (x + y * this.width); - - let bottom = Math.max(Math.min(y + height, this.height), 1); - while (y < --bottom) - this.bitmap.copyWithin(4 * (x + bottom * this.width), start, end); - - return this; - } - - /** - * Draws a circle at the specified coordinates with the specified radius - * @param {number} x The center x position - * @param {number} y The center y position - * @param {number} radius The circles radius - * @param {number|colorFunction} color - * @returns {Image} - */ - drawCircle(x, y, radius, color) { - const radSquared = radius ** 2; - for (let currentY = Math.max(1, y - radius); currentY <= Math.min(y + radius, this.height); currentY++) { - for (let currentX = Math.max(1, x - radius); currentX <= Math.min(x + radius, this.width); currentX++) { - if ((currentX - x) ** 2 + (currentY - y) ** 2 < radSquared) - this.__set_pixel__(currentX, currentY, typeof color === 'function' ? color(currentX - x + radius, currentY - y + radius) : color); - } - } - - return this; - } - - /** - * Crops the image into a circle - * @param {boolean} [max=false] Whether to use the larger dimension for the size - * @param {number} [feathering=0] How much feathering to apply to the edges - * @returns {Image} - */ - cropCircle(max = false, feathering = 0) { - new v2(this.width, this.height, this.bitmap).crop('circle', feathering); - - return this; - } - - /** - * Sets the images opacity - * @param {number} opacity The opacity to apply (0..1) - * @param {boolean} absolute Whether to scale the current opacity (false) or just set the new opacity (true) - * @returns {Image} - */ - opacity(opacity, absolute = false) { - if (isNaN(opacity) || opacity < 0) - throw new RangeError('Invalid opacity value'); - - this.__set_channel_value__(opacity, absolute, 3); - - return this; - } - - /** - * Sets the red channels saturation - * @param {number} saturation The saturation to apply (0..1) - * @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true) - * @returns {Image} - */ - red(saturation, absolute = false) { - if (isNaN(saturation) || saturation < 0) - throw new RangeError('Invalid saturation value'); - - this.__set_channel_value__(saturation, absolute, 0); - - return this; - } - - /** - * Sets the green channels saturation - * @param {number} saturation The saturation to apply (0..1) - * @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true) - * @returns {Image} - */ - green(saturation, absolute = false) { - if (isNaN(saturation) || saturation < 0) - throw new RangeError('Invalid saturation value'); - - this.__set_channel_value__(saturation, absolute, 1); - - return this; - } - - /** - * Sets the blue channels saturation - * @param {number} saturation The saturation to apply (0..1) - * @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true) - * @returns {Image} - */ - blue(saturation, absolute = false) { - if (isNaN(saturation) || saturation < 0) - throw new RangeError('Invalid saturation value'); - - this.__set_channel_value__(saturation, absolute, 2); - - return this; - } - - /** - * @private - * @param {number} value - * @param {boolean} absolute - * @param {number} offset - */ - __set_channel_value__(value, absolute, offset) { - for (let i = offset; i < this.bitmap.length; i += 4) - this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]); - } - - /** - * Sets the brightness of the image - * @param {number} value The lightness to apply (0..1) - * @param {boolean} absolute Whether to scale the current lightness (false) or just set the new lightness (true) - * @returns {Image} - */ - lightness(value, absolute = false) { - if (isNaN(value) || value < 0) - throw new RangeError('Invalid lightness value'); - - return this.fill((x, y) => { - const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y)); - return Image.hslaToColor(h, s, value * (absolute ? 1 : l), a); - }); - } - - /** - * Sets the saturation of the image - * @param {number} value The saturation to apply (0..1) - * @param {boolean} absolute Whether to scale the current saturation (false) or just set the new saturation (true) - * @returns {Image} - */ - saturation(value, absolute = false) { - if (isNaN(value) || value < 0) - throw new RangeError('Invalid saturation value'); - - return this.fill((x, y) => { - const [h, s, l, a] = Image.rgbaToHSLA(...this.getRGBAAt(x, y)); - return Image.hslaToColor(h, value * (absolute ? 1 : s), l, a); - }); - } - - /** - * Composites (overlays) the source onto this image at the specified coordinates - * @param {Image} source The image to place - * @param {number} [x=0] The x position to place the image at - * @param {number} [y=0] The y position to place the image at - * @returns {Image} - */ - composite(source, x = 0, y = 0) { - new v2(this.width, this.height, this.bitmap).overlay(new v2(source.width, source.height, source.bitmap), x, y); - - return this; - } - - /** - * Inverts the images colors - * @returns {Image} - */ - invert() { - for (const [x, y, color] of this.iterateWithColors()) - this.__set_pixel__(x, y, ((0xffffffff - color) & 0xffffff00) | (color & 0xff)); - - return this; - } - - /** - * Inverts the images value (lightness) - * @returns {Image} - */ - invertValue() { - for (const [x, y, color] of this.iterateWithColors()) { - const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color)); - this.__set_pixel__(x, y, Image.hslaToColor(h, s, 1 - l, a)); - } - - return this; - } - - /** - * Inverts the images saturation - * @returns {Image} - */ - invertSaturation() { - for (const [x, y, color] of this.iterateWithColors()) { - const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color)); - this.__set_pixel__(x, y, Image.hslaToColor(h, 1 - s, l, a)); - } - - return this; - } - - /** - * Inverts the images hue - * @returns {Image} - */ - invertHue() { - for (const [x, y, color] of this.iterateWithColors()) { - const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color)); - this.__set_pixel__(x, y, Image.hslaToColor(1 - h, s, l, a)); - } - - return this; - } - - /** - * Shifts the images hue - * @param {number} degrees How many degrees to shift the hue by - */ - hueShift(degrees) { - for (const [x, y, color] of this.iterateWithColors()) { - const [h, s, l, a] = Image.rgbaToHSLA(...Image.colorToRGBA(color)); - this.__set_pixel__(x, y, Image.hslaToColor(h + degrees / 360, s, l, a)); - } - - return this; - } - - /** - * Gets the average color of the image - * @returns {number} - */ - averageColor() { - let colorAvg = [0, 0, 0]; - let divisor = 0; - for (let idx = 0; idx < this.bitmap.length; idx += 4) { - const rgba = this.bitmap.subarray(idx, idx + 4); - for (let i = 0; i < 3; i++) - colorAvg[i] += rgba[i]; - divisor += rgba[3] / 255; - } - - return Image.rgbaToColor(...colorAvg.map(v => v / divisor), 0xff); - } - - /** - * Gets the images dominant color - * @param {boolean} [ignoreBlack=true] Whether to ignore dark colors below the threshold - * @param {boolean} [ignoreWhite=true] Whether to ignore light colors above the threshold - * @param {number} [bwThreshold=0xf] The black/white threshold (0-64) - * @return {number} The images dominant color - */ - dominantColor(ignoreBlack = true, ignoreWhite = true, bwThreshold = 0xf) { - const colorCounts = new Array(0x3ffff); - for (let i = 0; i < this.bitmap.length; i += 4) { - const color = this.__view__.getUint32(i, false); - const [h, s, l] = Image.rgbaToHSLA(...Image.colorToRGBA(color)).map(v => (~~(v * 0x3f))); - if (ignoreBlack && l < bwThreshold) continue; - if (ignoreWhite && l > 0x3f - bwThreshold) continue; - const key = h << 12 | s << 6 | l; - colorCounts[key] = (colorCounts[key] || 0) + 1; - } - - let maxColorCount = -1; - let mostProminentValue = 0; - colorCounts.forEach((el, i) => { - if (el < maxColorCount) return; - maxColorCount = el; - mostProminentValue = i; - }); - - if (mostProminentValue === -1) - return this.dominantColor(ignoreBlack, ignoreWhite, bwThreshold - 1); - - const h = (mostProminentValue >>> 12) & 0x3f; - const s = (mostProminentValue >>> 6) & 0x3f; - const l = mostProminentValue & 0x3f; - - return Image.hslaToColor(h / 0x3f, s / 0x3f, l / 0x3f, 1); - } - - /** - * Rotates the image the given amount of degrees - * @param {number} angle The angle to rotate the image for (in degrees) - * @param {boolean} resize Whether to resize the image so it fits all pixels or just ignore outlying pixels - */ - rotate(angle, resize = true) { - const frame = new v2(this.width, this.height, this.bitmap).rotate(360 - (angle % 360), resize); - - const out = new Image(frame.width, frame.height); - - out.bitmap.set(frame.u8); - return this.__apply__(out); - } - - /** - * Flips / mirrors the image horizontally or vertically - * @param {'horizontal' | 'vertical'} direction The direction to flip - */ - flip(direction) { - const frame = new v2(this.width, this.height, this.bitmap).flip(direction); - - this.bitmap.set(frame.u8); - return this; - } - - /** - * @private - * @param {Image|Frame} image - * @returns {Image|Frame} - */ - __apply__(image) { - this.__width__ = image.__width__; - this.__height__ = image.__height__; - this.__view__ = image.__view__; - this.__u32__ = image.__u32__; - this.bitmap = image.bitmap; - - if (image instanceof Frame) - return Frame.from(this, image.duration, image.xOffset, image.yOffset, image.disposalMode); - - return this; - } - - /** - * Creates a multi-point gradient generator - * @param {Object} colors The gradient points to use (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`) - * @return {(function(number): number)} The gradient generator. The function argument is the position in the gradient (0..1). - */ - static gradient(colors) { - const entries = Object.entries(colors).sort((a, b) => a[0] - b[0]); - const positions = entries.map(e => parseFloat(e[0])); - const values = entries.map(e => e[1]); - - if (positions.length === 0) throw new RangeError('Invalid gradient point count'); - else if (positions.length === 1) { - return () => values[0]; - } else if (positions.length === 2) { - const gradient = this.__gradient__(values[0], values[1]); - return position => { - if (position <= positions[0]) return values[0]; - if (position >= positions[1]) return values[1]; - return gradient((position - positions[0]) / (positions[1] - positions[0])); - }; - } - - const minDef = Math.min(...positions); - const maxDef = Math.max(...positions); - let gradients = []; - - for (let i = 0; i < positions.length; i++) { - let minPos = positions[i - 1]; - if (minPos === undefined) continue; - - let maxPos = positions[i]; - - let minVal = values[i - 1]; - if (minVal === undefined) minVal = values[i]; - - const maxVal = values[i]; - const gradient = this.__gradient__(minVal, maxVal); - - gradients.push({min: minPos, max: maxPos, gradient}); - } - - return position => { - if (position <= minDef) return gradients[0].gradient(0); - if (position >= maxDef) return gradients[gradients.length - 1].gradient(1); - - for (const gradient of gradients) - if (position >= gradient.min && position <= gradient.max) - return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min)); - throw new RangeError(`Invalid gradient position: ${position}`); - }; - } - - /** - * Rounds the images corners - * @param {number} [radius=min(width,height)/4] The radius of the corners - * @return {Image} - */ - roundCorners(radius = Math.min(this.width, this.height) / 4) { - const radSquared = radius ** 2; - for (let x = 1; x <= radius; x++) { - const xRad = (x - radius) ** 2; - for (let y = 1; y <= radius; y++) { - if (xRad + (y - radius) ** 2 > radSquared) - this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0; - } - } - - for (let x = 1; x <= radius; x++) { - const xRad = (x - radius) ** 2; - for (let y = this.height - radius; y <= this.height; y++) { - if (xRad + ((this.height - y) - radius) ** 2 > radSquared) - this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0; - } - } - - for (let x = this.width - radius; x <= this.width; x++) { - const xRad = ((this.width - x) - radius) ** 2; - for (let y = 1; y <= radius; y++) { - if (xRad + (y - radius) ** 2 > radSquared) - this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0; - } - } - - for (let x = this.width - radius; x <= this.width; x++) { - const xRad = ((this.width - x) - radius) ** 2; - for (let y = this.height - radius; y <= this.height; y++) { - if (xRad + ((this.height - y) - radius) ** 2 > radSquared) - this.bitmap[((y - 1) * this.width + x - 1) * 4 + 3] = 0; - } - } - - return this; - } - - /** - * @private - */ - static __gradient__(startColor, endColor) { - const sr = startColor >>> 24; - const sg = startColor >> 16 & 0xff; - const sb = startColor >> 8 & 0xff; - const sa = startColor & 0xff; - const er = (endColor >>> 24) - sr; - const eg = (endColor >> 16 & 0xff) - sg; - const eb = (endColor >> 8 & 0xff) - sb; - const ea = (endColor & 0xff) - sa; - - return position => { - const r = sr + position * er; - const g = sg + position * eg; - const b = sb + position * eb; - const a = sa + position * ea; - return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)); - }; - } - - fisheye(radius = 2) { - const r = new Image(this.width, this.height); - - const w = this.width; - const h = this.height; - const tu32 = this.__u32__; - const ru32 = r.__u32__; - const iw = 1 / w; - const ih = 1 / h; - - for (const [x, y] of this) { - const xco = x * iw - .5; - const yco = y * ih - .5; - const dfc = Math.sqrt(xco ** 2 + yco ** 2); - const dis = 2 * dfc ** radius; - const nx = ((dis * xco / dfc + 0.5) * w) | 0; - const ny = ((dis * yco / dfc + 0.5) * h) | 0; - - if (nx < 1 || nx > w || ny < 1 || ny > h || isNaN(nx) || isNaN(ny)) - continue; - - ru32[y * w + x] = tu32[w * ny + nx]; - } - - const cO = tu32.length * .5 + w / 2; - ru32[cO] = tu32[cO]; - - return this.__apply__(r); - } - - /** - * @typedef {object} PNGMetadata - * @property {string} [title] The images title - * @property {string} [author] The images author - * @property {string} [description] The images description - * @property {string} [copyright] The images copyright info - * @property {string|number|Date} [creationTime=Date.now()] The images creation timestamp - * @property {string} [software="github.com/matmen/ImageScript vX.X.X"] The software used to create this image - * @property {string} [disclaimer] A disclaimer for the image - * @property {string} [warning] A warning for the image - * @property {string} [source] The images source - * @property {string} [comment] A comment for the image - */ - - /** - * Encodes the image into a PNG - * @param {number} compression The compression level to use (0-9) - * @param {PNGMetadata} [meta={}] Image metadata - * @return {Promise} The encoded data - */ - async encode(compression = 1, { - title, - author, - description, - copyright, - creationTime, - software, - disclaimer, - warning, - source, - comment - } = {}) { - return png.encode(this.bitmap, { - width: this.width, - height: this.height, - level: compression, - channels: 4, - text: { - Title: title, - Author: author, - Description: description, - Copyright: copyright, - 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(), - Software: software === undefined ? `github.com/matmen/ImageScript v${version}` : software, - Disclaimer: disclaimer, - Warning: warning, - Source: source, - Comment: comment - } - }); - } - - /** - * Encodes the image into a JPEG - * @param {number} [quality=90] The JPEG quality to use (1-100) - * @return {Promise} - */ - async encodeJPEG(quality = 90) { - return codecs.jpeg.encode_async(this.bitmap, { - quality, - width: this.width, - height: this.height, - }); - } - - /** - * Encodes the image into a WEBP - * @param {null|number} [quality=null] The WEBP quality to use (0-100) (null is lossless) - * @return {Promise} - */ - async encodeWEBP(quality = null) { - return codecs.webp.encode_async(this.bitmap, { - quality, - width: this.width, - height: this.height, - }); - } - - /** - * Decodes an image (PNG, JPEG or TIFF) - * @param {Buffer|Uint8Array} data The binary data to decode - * @return {Promise} The decoded image - */ - static async decode(data) { - let image; - - data = mem.view(data); - const view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - if (ImageType.isPNG(view)) { // PNG - const {width, height, framebuffer} = (await pnglib.init()).decode(data); - image = new Image(width, height); - image.bitmap.set(framebuffer); - } else if (ImageType.isJPEG(view)) { // JPEG - const framebuffer = (await jpeglib.init()).decode(data); - - const width = framebuffer.width; - const height = framebuffer.height; - const pixelType = framebuffer.format; - - image = new Image(width, height); - const buffer = framebuffer.buffer; - - if (pixelType === 0) { - const view = new DataView(image.bitmap.buffer); - - for (let i = 0; i < buffer.length; i++) { - const pixel = buffer[i]; - view.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 0xff, false); - } - } else if (pixelType === 1) { - image.bitmap.fill(0xff); - for (let i = 0; i < width * height; i++) - image.bitmap.set(buffer.subarray(i * 3, i * 3 + 3), i * 4); - } else if (pixelType === 2) { - for (let i = 0; i < buffer.length; i += 4) { - image.bitmap[i] = 0xff * (1 - buffer[i] / 0xff) * (1 - buffer[i + 3] / 0xff); - image.bitmap[i + 1] = 0xff * (1 - buffer[i + 1] / 0xff) * (1 - buffer[i + 3] / 0xff); - image.bitmap[i + 2] = 0xff * (1 - buffer[i + 2] / 0xff) * (1 - buffer[i + 3] / 0xff); - image.bitmap[i + 3] = 0xff; - } - } - } else if (ImageType.isTIFF(view)) { // TIFF - const framebuffer = (await tifflib.init()).decode(data); - image = new Image(framebuffer.width, framebuffer.height); - - image.bitmap.set(framebuffer.buffer); - } else throw new Error('Unsupported image type'); - - return image; - } - - /** - * Scale the SVG by the given amount. For use with {@link Image.renderSVG} - * @return {number} - */ - static get SVG_MODE_SCALE() { - return 1; - } - - /** - * Scale the SVG to fit the given width. For use with {@link Image.renderSVG} - * @return {number} - */ - static get SVG_MODE_WIDTH() { - return 2; - } - - /** - * Scale the SVG to fit the given height. For use with {@link Image.renderSVG} - * @return {number} - */ - static get SVG_MODE_HEIGHT() { - return 3; - } - - /** - * Creates a new image from the given SVG - * @param {string} svg The SVG content - * @param {number} size The size to use - * @param {number} mode The SVG resizing mode to use (one of {@link SVG_MODE_SCALE}, {@link SVG_MODE_WIDTH}, {@link SVG_MODE_HEIGHT}) - * @return {Promise} The rendered SVG graphic - */ - static async renderSVG(svg, size = 1, mode = this.SVG_MODE_SCALE) { - if (![this.SVG_MODE_WIDTH, this.SVG_MODE_HEIGHT, this.SVG_MODE_SCALE].includes(mode)) - throw new Error('Invalid SVG scaling mode'); - - if (mode === this.SVG_MODE_SCALE && size <= 0) - throw new RangeError('SVG scale must be > 0'); - if (mode !== this.SVG_MODE_SCALE && size < 1) - throw new RangeError('SVG size must be >= 1') - - if (typeof svg === 'string') svg = Buffer.from(svg); - const framebuffer = (await svglib.init()).rasterize(svg, mode, size); - - const image = new Image(framebuffer.width, framebuffer.height); - - image.bitmap.set(framebuffer.buffer); - - return image; - } - - /** - * Creates a new image containing the rendered text. - * @param {Uint8Array} font TrueType (ttf/ttc) or OpenType (otf) font buffer to use - * @param {number} scale Font size to use - * @param {string} text Text to render - * @param {number} [color=0xffffffff] Text color to use - * @param {TextLayout} [layout] The text layout to use - * @return {Promise} The rendered text - */ - static async renderText(font, scale, text, color = 0xffffffff, layout = new TextLayout()) { - const { Font, Layout } = await fontlib.init(); - - font = new Font(scale, font); - const [r, g, b, a] = Image.colorToRGBA(color); - - const layoutOptions = new Layout(); - - layoutOptions.reset({ - max_width: layout.maxWidth, - max_height: layout.maxHeight, - wrap_style: layout.wrapStyle, - vertical_align: layout.verticalAlign, - horizontal_align: layout.horizontalAlign, - wrap_hard_breaks: layout.wrapHardBreaks - }); - - layoutOptions.append(font, text, {scale}); - const framebuffer = layoutOptions.rasterize(r, g, b); - const image = new Image(framebuffer.width, framebuffer.height); - - image.bitmap.set(framebuffer.buffer); - - if (image.height > layout.maxHeight) - image.crop(0, 0, image.width, Math.floor(layoutOptions.lines() / image.height * layout.maxHeight) * (image.height / layoutOptions.lines())); - - font.free(); - layoutOptions.free(); - return image.opacity(a / 0xff); - } - -} - -/** - * Represents a frame in a GIF - * @extends Image - */ -class Frame extends Image { - /** - * GIF frame disposal mode KEEP. For use with {@link Frame} - * @returns {string} - */ - static get DISPOSAL_KEEP() { - return 'keep'; - } - - /** - * GIF frame disposal mode PREVIOUS. For use with {@link Frame} - * @returns {string} - */ - static get DISPOSAL_PREVIOUS() { - return 'previous'; - } - - /** - * GIF frame disposal mode BACKGROUND. For use with {@link Frame} - * @returns {string} - */ - static get DISPOSAL_BACKGROUND() { - return 'background'; - } - - static __convert_disposal_mode__(mode) { - if (typeof mode === 'string') - mode = ['any', 'keep', 'previous', 'background'].indexOf(mode); - if (mode < 0 || mode > 3) - throw new RangeError('Invalid disposal mode'); - - return mode; - } - - /** - * Creates a new, blank frame - * @param {number} width - * @param {number} height - * @param {number} [duration = 100] The frames duration (in ms) - * @param {number} [xOffset=0] The frames offset on the x-axis - * @param {number} [yOffset=0] The frames offset on the y-axis - * @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frame's disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND}) - * @return {Frame} - */ - constructor(width, height, duration = 100, xOffset = 0, yOffset = 0, disposalMode = Frame.DISPOSAL_KEEP) { - if (isNaN(duration) || duration < 0) - throw new RangeError('Invalid frame duration'); - - super(width, height); - this.duration = duration; - this.xOffset = xOffset; - this.yOffset = yOffset; - this.disposalMode = disposalMode; - } - - /** - * The Frame's disposal mode - * @returns {number} - */ - get disposalMode() { - return this.__disposalMode__; - } - - /** - * Sets the frame's disposal mode, converting it to the internal numeric value. - * @param {string|number} disposalMode The frame's disposal mode - */ - set disposalMode(disposalMode) { - this.__disposalMode__ = Frame.__convert_disposal_mode__(disposalMode); - } - - toString() { - return `Frame<${this.width}x${this.height}x${this.duration}ms>`; - } - - /** - * Converts an Image instance to a Frame, cloning it in the process - * @param {Image} image The image to create the frame from - * @param {number} [duration = 100] The frames duration (in ms) - * @param {number} [xOffset=0] The frames offset on the x-axis - * @param {number} [yOffset=0] The frames offset on the y-axis - * @param {string|number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode ({@link Frame.DISPOSAL_KEEP}, {@link Frame.DISPOSAL_PREVIOUS} or {@link Frame.DISPOSAL_BACKGROUND}) - * @return {Frame} - */ - static from(image, duration, xOffset, yOffset, disposalMode = Frame.DISPOSAL_KEEP) { - if (!(image instanceof Image)) - throw new TypeError('Invalid image passed'); - - const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode); - frame.bitmap.set(image.bitmap); - - return frame; - } - - resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - const originalWidth = this.width; - const originalHeight = this.height; - - const result = super.resize(width, height, mode); - - this.xOffset *= result.width / originalWidth; - this.yOffset *= result.height / originalHeight; - - return result; - } -} - -/** - * Represents a GIF image as an array of frames - * @extends Array - */ -class GIF extends Array { - /** - * Creates a new GIF image. - * @param {Frame[]} frames The frames to create the GIF from - * @param {number} [loopCount=-1] How often to loop the GIF for (-1 = unlimited) - * @property {number} loopCount How often the GIF will loop for - */ - constructor(frames, loopCount = -1) { - super(...frames); - - for (const frame of this) - if (!(frame instanceof Frame)) - throw new TypeError(`Frame ${this.indexOf(frame)} is not an instance of Frame`); - - if (loopCount < -1 || isNaN(loopCount)) - throw new RangeError('Invalid loop count'); - - this.loopCount = loopCount; - } - - /** - * The GIFs width - * @returns {number} - */ - get width() { - let max = 0; - for (const frame of this) { - let width = frame.width + frame.xOffset; - if (max < width) - max = width; - } - - return max; - } - - /** - * The GIFs height - * @returns {number} - */ - get height() { - let max = 0; - for (const frame of this) { - let height = frame.height + frame.yOffset; - if (max < height) - max = height; - } - - return max; - } - - toString() { - return `GIF<${this.width}x${this.height}x${this.duration}ms>`; - } - - /** - * @returns {Generator} - */ - * [Symbol.iterator]() { - for (let i = 0; i < this.length; i++) - yield this[i]; - } - - slice(start, end) { - if (end === Infinity) - end = this.length; - const frames = new Array(end - start); - for (let i = 0; i < frames.length; i++) - frames[i] = this[i + start]; - return new GIF(frames, this.loopCount); - } - - /** - * The GIFs duration (in ms) - * @return {number} - */ - get duration() { - return this.reduce((acc, frame) => acc + frame.duration, 0); - } - - /** - * Encodes the image into a GIF - * @param {number} [quality=95] GIF quality 0-100 - * @return {Promise} The encoded data - */ - async encode(quality = 95) { - const encoder = new codecs.gif.encoder(this.width, this.height); - - for (const frame of this) { - if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames'); - - encoder.add(frame.bitmap, { - quality, - x: frame.xOffset, - y: frame.yOffset, - width: frame.width, - speed: null, // 1-10 - height: frame.height, - colors: null, // 2-256 - delay: ~~(frame.duration / 10), - dispose: ['any', 'keep', 'previous', 'background'][frame.disposalMode], - }); - } - - return encoder.finish({ repeat: -1 === this.loopCount ? null : this.loopCount }); - } - - /** - * Decodes a GIF image - * @param {Buffer|Uint8Array} data The binary data to decode - * @param {boolean} [onlyExtractFirstFrame=false] Whether to end GIF decoding after the first frame - * @return {Promise} The decoded GIF - */ - static async decode(data, onlyExtractFirstFrame = false) { - let image; - data = mem.view(data); - const view = new DataView(data.buffer, data.byteOffset, data.byteLength); - - if (ImageType.isGIF(view)) { // GIF - const frames = []; - const decoder = new (await giflib.init()).Decoder(data); - - const gwidth = decoder.width | 0; - const gheight = decoder.height | 0; - const u32 = new Uint32Array(decoder.width * decoder.height); - const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength); - - for (const frame of decoder.frames()) { - let offset8 = 0 | 0; - let offset32 = 0 | 0; - const fx = frame.x | 0; - const fy = frame.y | 0; - const f8 = frame.buffer; - const mode = frame.dispose; - const width = frame.width | 0; - const height = frame.height | 0; - const f32 = new Uint32Array(f8.buffer, f8.byteOffset, width * height); - const f = frames[frames.push(new Frame(gwidth, gheight, 10 * frame.delay, 0, 0, 3)) - 1]; - - const t8 = f.bitmap; - const t32 = new Uint32Array(t8.buffer); - - t8.set(u8); - - if (2 === mode) { - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - } - } - } - - else if (3 === mode) { - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - u32[x_offset] = 0; - } - } - } - - else if (0 === mode || 1 === mode) { - t8.set(u8); - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - u32[x_offset] = t32[x_offset]; - } - } - } - - if (onlyExtractFirstFrame) - break; - } - - image = new GIF(frames); - } else throw new Error('Unsupported image type'); - - return image; - } - - resize(width, height, mode = Image.RESIZE_NEAREST_NEIGHBOR) { - for (const frame of this) - frame.resize(width, height, mode); - } -} - -class TextLayout { - /** - * Layout options for {@link Image.renderText} - * @param {object} [options] - * @param {number} [options.maxWidth=Infinity] The texts max width - * @param {number} [options.maxHeight=Infinity] The texts max height - * @param {string} [options.wrapStyle='word'] The texts wrap style when reaching the max width (word, char) - * @param {string} [options.verticalAlign='left'] The vertical align mode (left, center, right) - * @param {string} [options.horizontalAlign='top'] The horizontal align mode (top, middle, bottom) - * @param {boolean} [options.wrapHardBreaks=true] Whether to force wrap at new line characters - */ - constructor(options) { - const {maxWidth, maxHeight, wrapStyle, verticalAlign, horizontalAlign, wrapHardBreaks} = options || {}; - - this.maxWidth = maxWidth || Infinity; - if (isNaN(this.maxWidth) || this.maxWidth < 1) - throw new RangeError('Invalid maxWidth'); - - this.maxHeight = maxHeight || Infinity; - if (isNaN(this.maxHeight) || this.maxHeight < 1) - throw new RangeError('Invalid maxHeight'); - - this.wrapStyle = wrapStyle || 'word'; - if (!['word', 'char'].includes(this.wrapStyle)) - throw new RangeError('Invalid wrapStyle'); - - this.verticalAlign = verticalAlign || 'left'; - if (!['left', 'center', 'right'].includes(this.verticalAlign)) - throw new RangeError('Invalid verticalAlign'); - - this.horizontalAlign = horizontalAlign || 'top'; - if (!['top', 'middle', 'bottom'].includes(this.horizontalAlign)) - throw new RangeError('Invalid horizontalAlign'); - - this.wrapHardBreaks = typeof wrapHardBreaks === 'undefined' ? true : wrapHardBreaks; - if (typeof this.wrapHardBreaks !== 'boolean') - throw new TypeError('Invalid wrapHardBreaks'); - } -} - -class ImageType { - /** - * Gets an images type (png, jpeg, tiff, gif) - * @param {Buffer|Uint8Array} data The image binary to get the type of - * @returns {string|null} The image type (png, jpeg, tiff, gif, null) - */ - static getType(data) { - let view; - if (!ArrayBuffer.isView(data)) { - data = new Uint8Array(data); - view = new DataView(data.buffer); - } else { - data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - view = new DataView(data.buffer, data.byteOffset, data.byteLength); - } - - if (this.isPNG(view)) return 'png'; - if (this.isJPEG(view)) return 'jpeg'; - if (this.isTIFF(view)) return 'tiff'; - if (this.isGIF(view)) return 'gif'; - return null; - } - - /** - * @param {DataView} view - * @returns {boolean} - */ - static isPNG(view) { - return view.byteLength >= 4 && view.getUint32(0, false) === MAGIC_NUMBERS.PNG; - } - - /** - * @param {DataView} view - * @returns {boolean} - */ - static isJPEG(view) { - return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.JPEG; - } - - /** - * @param {DataView} view - * @returns {boolean} - */ - static isTIFF(view) { - return view.byteLength >= 4 && view.getUint32(0, false) === MAGIC_NUMBERS.TIFF; - } - - /** - * @param {DataView} view - * @returns {boolean} - */ - static isGIF(view) { - return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF; - } -} - -/** - * Decodes the given image binary - * @param {Uint8Array|Buffer} data The image data - * @param {boolean} [onlyExtractFirstFrame] Whether to end GIF decoding after the first frame - * @returns {Promise} The decoded image - */ -function decode(data, onlyExtractFirstFrame) { - const type = ImageType.getType(data); - - if (type === 'gif') - return GIF.decode(data, onlyExtractFirstFrame); - return Image.decode(data); -} - -module.exports = {Image, GIF, Frame, TextLayout, ImageType, decode}; diff --git a/LICENSE b/LICENSE index 5d2197d..3c1eb49 100644 --- a/LICENSE +++ b/LICENSE @@ -9,4 +9,7 @@ mentioned licenses, but a license must be chosen. The corresponding license files can be found in the projects root directory, prefixed with LICENSE, suffixed with their -corresponding SPDX identifier. \ No newline at end of file +corresponding SPDX identifier. + +Parts of this software are derived from third party projects under their +own licenses (MIT, Apache-2.0 and the IJG license), see THIRD_PARTY_NOTICES.md. \ No newline at end of file diff --git a/README.md b/README.md index 4648c83..0a2bf12 100644 --- a/README.md +++ b/README.md @@ -3,49 +3,87 @@ [![Discord Server](https://img.shields.io/discord/691713541262147687.svg?label=Discord&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2&style=for-the-badge)](https://discord.gg/8hPrwAH) [![Documentation](https://img.shields.io/badge/Documentation-informational?style=for-the-badge)](https://imagescript.matmen.dev/) [![Github](https://img.shields.io/badge/Github-Repository-181717?logo=github&style=for-the-badge)](https://github.com/matmen/ImageScript) -[![deno.land](https://shields.io/badge/deno.land-gray?logo=deno&style=for-the-badge)](https://deno.land/x/imagescript) [![NPM](https://nodei.co/npm/imagescript.png)](https://www.npmjs.com/package/imagescript) --- -**ImageScript** is a zero-dependency alternative to common JavaScript bitmap image manipulation tools. It can achieve -much more performant results by utilizing lower-level memory access, less memory copying and WebAssembly / native -binaries for decoding and encoding. +**ImageScript** is a zero-dependency alternative to common JavaScript bitmap image manipulation tools. Every codec and +renderer is written in TypeScript: no WebAssembly, no native binaries, no npm dependencies, no install scripts. The +package is about 120 kB (compressed) and runs in Node.js 22.13 or newer (including Alpine/musl, ARM and bundled +serverless functions), Deno, Bun and browsers. It works with both `import` and `require()`, type definitions are included. + +```sh +npm install imagescript # Node.js, Bun and bundlers +deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too) +``` --- ### Features -- [Decoding images](https://imagescript.matmen.dev/Image.html#.decode) - - PNGs (grayscale, RGB, indexed colors) with and without alpha channels - - JPEGs (grayscale, RGB, CMYK) - - TIFFs -- [Decoding GIFs](https://imagescript.matmen.dev/GIF.html#.decode) -- [Rendering SVGs](https://imagescript.matmen.dev/Image.html#.renderSVG) -- [Rendering vector fonts](https://imagescript.matmen.dev/Image.html#.renderText) -- Image manipulation functions ([crop](https://imagescript.matmen.dev/Image.html#crop) - , [rotate](https://imagescript.matmen.dev/Image.html#rotate) - , [composite](https://imagescript.matmen.dev/Image.html#composite), ...) -- Color manipulation functions ([invert](https://imagescript.matmen.dev/Image.html##invert) - , [hueShift](https://imagescript.matmen.dev/Image.html##hueshift), ...) -- Color information functions ([averageColor](https://imagescript.matmen.dev/Image.html#averageColor) - , [dominantColor](https://imagescript.matmen.dev/Image.html#dominantColor), ...) -- Encoding images as [PNGs](https://imagescript.matmen.dev/Image.html#encode) - , [JPEGs](https://imagescript.matmen.dev/Image.html#encodejpeg) - , [WEBPs](https://imagescript.matmen.dev/Image.html#encodeWEBP) - and [GIFs](https://imagescript.matmen.dev/GIF.html#encode) +- [Decoding images](https://imagescript.matmen.dev/classes/Image.html#decode) + - PNGs (grayscale, RGB, indexed colors, 1-16 bit, interlaced) with and without alpha channels + - JPEGs (baseline and progressive; grayscale, RGB, CMYK) + - TIFFs (strips and tiles; uncompressed, LZW, deflate, PackBits and JPEG compressed; BigTIFF) + - JPEGs, TIFFs and PNGs are rotated as their Exif (or, for PNGs, XMP) orientation says +- [Decoding GIFs](https://imagescript.matmen.dev/classes/GIF.html#decode) with all frames, durations and disposal modes +- [Rendering SVGs](https://imagescript.matmen.dev/classes/Image.html#rendersvg) (shapes, paths, gradients, strokes, clip paths, + masks, CSS; no text, images or filters) +- [Rendering vector fonts](https://imagescript.matmen.dev/classes/Image.html#rendertext) (TrueType and OpenType, with kerning and outlines) +- [Resizing](https://imagescript.matmen.dev/classes/Image.html#resize) (bilinear, bicubic, area averaging and nearest neighbor) +- Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop) + , [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate) + , [cropCircle](https://imagescript.matmen.dev/classes/Image.html#cropcircle) + , [roundCorners](https://imagescript.matmen.dev/classes/Image.html#roundcorners), ...) +- Filters and color adjustments ([blur](https://imagescript.matmen.dev/classes/Image.html#blur) + , [sharpen](https://imagescript.matmen.dev/classes/Image.html#sharpen) + , [brightness](https://imagescript.matmen.dev/classes/Image.html#brightness) + , [hueShift](https://imagescript.matmen.dev/classes/Image.html#hueshift), ...) +- Compositing with opacity and blend modes ([composite](https://imagescript.matmen.dev/classes/Image.html#composite)) +- Color information functions ([averageColor](https://imagescript.matmen.dev/classes/Image.html#averagecolor) + , [dominantColor](https://imagescript.matmen.dev/classes/Image.html#dominantcolor), ...) +- Encoding images as [PNGs](https://imagescript.matmen.dev/classes/Image.html#encode) (smallest lossless color type) + , [JPEGs](https://imagescript.matmen.dev/classes/Image.html#encodejpeg) + and animated [GIFs](https://imagescript.matmen.dev/classes/GIF.html#encode) --- ### Example -Check out one of these examples: -* **NodeJS**: [README image generation](https://github.com/matmen/ImageScript/blob/master/tests/readme.js) -* **Deno**: [README image generation](https://github.com/matmen/ImageScript/blob/deno/tests/readme.js) -* **Browser**: [Grayscale Conversion Example Page](https://github.com/matmen/ImageScript/blob/browser/example/index.html) (via CDN) +```js +import { Image } from 'imagescript'; +import { readFile, writeFile } from 'node:fs/promises'; + +const image = await Image.decode(await readFile('input.jpg')); +image.resize(256, Image.RESIZE_AUTO).roundCorners(16); +await writeFile('output.png', await image.encode()); +``` + +With CommonJS: + +```js +const { Image } = require('imagescript'); +``` + +All coordinates are 0-based, `(0, 0)` is the top left pixel. + +A bigger example is the [README image generation](https://github.com/matmen/ImageScript/blob/master/tests/readme.js). +Upgrading from 1.x? Version 3 has breaking changes (0-based coordinates, removed WebP encoding, Node.js 22.13+, ...), +see the [changelog](https://github.com/matmen/ImageScript/blob/master/CHANGELOG.md). There is no 2.x release, the number was skipped because of the old experimental +`imagescript/v2` API. [![Example](https://github.com/matmen/ImageScript/raw/master/tests/targets/readme.png)](https://github.com/matmen/ImageScript/blob/master/tests/readme.js) --- +### Credits + +The deflate/inflate implementation is derived from [fflate](https://github.com/101arrowz/fflate) and the CRC-32 from +[Fast CRC32 in JavaScript](https://gist.github.com/101arrowz/e58695f7ccfdf74f60ba22018093edea), both by Arjun Barrett. +The JPEG codec is based in part on the work of the Independent JPEG Group (libjpeg) and on +[pdf.js](https://github.com/mozilla/pdf.js), the glyph rasterizer is translated from +[font-rs](https://github.com/raphlinus/font-rs). See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for their licenses. + +--- + If you have any additional questions, feel free to join the [discord support server](https://discord.gg/8hPrwAH). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..2247069 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,74 @@ +# Third party notices + +ImageScript is licensed under AGPL-3.0-or-later OR MIT (see [LICENSE](LICENSE)). Parts of it are derived from the +following projects, under their own licenses. Each derived file says so in its header. + +| Project | Used in | License | +|---|---|---| +| [fflate](https://github.com/101arrowz/fflate) by Arjun Barrett | `src/codecs/zlib.ts` (deflate/inflate) | MIT | +| [Fast CRC32 in JavaScript](https://gist.github.com/101arrowz/e58695f7ccfdf74f60ba22018093edea) by Arjun Barrett | `src/codecs/crc.ts` (slice-by-16 CRC-32) | MIT | +| [libjpeg](https://ijg.org/) / [libjpeg-turbo](https://libjpeg-turbo.org/) by the Independent JPEG Group | `src/codecs/jpeg.ts` (IDCT, FDCT, upsampling, color conversion, progressive refinement, optimal Huffman tables) | IJG | +| [pdf.js](https://github.com/mozilla/pdf.js) (forked from [jpgjs](https://github.com/notmasteryet/jpgjs) by notmasteryet) | `src/codecs/jpeg.ts` (structure of the scan decoder) | Apache-2.0 | +| [font-rs](https://github.com/raphlinus/font-rs) by Raph Levien | `src/render/text.ts` (the `Rasterizer` class, also used for SVGs) | Apache-2.0 | + +## fflate, Fast CRC32 in JavaScript + +``` +MIT License + +Copyright (c) 2020 Arjun Barrett (fflate) +Copyright (c) 2021 Arjun Barrett (Fast CRC32 in JavaScript) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## libjpeg / libjpeg-turbo + +This software is based in part on the work of the Independent JPEG Group. + +The JPEG codec translates parts of libjpeg (as shipped by libjpeg-turbo) to TypeScript: the integer inverse DCT +(`jidctint.c`), the floating point forward DCT (`jfdctflt.c`), fancy upsampling (`jdsample.c`), the YCbCr to RGB +tables (`jdcolor.c`), the progressive AC refinement (`jdphuff.c`) and the optimal Huffman table generation +(`jcphuff.c`/`jchuff.c`). The translations were rewritten for TypeScript's numbers and typed arrays; the rest of the +codec is ImageScript's own. libjpeg's README with its copyright and terms is included unaltered as +[licenses/README.ijg](licenses/README.ijg). + +## pdf.js, font-rs + +``` +pdf.js: Copyright 2014 Mozilla Foundation +font-rs: Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +The scan decoder of `src/codecs/jpeg.ts` follows the structure of pdf.js' `src/core/jpg.js` (block and MCU layout, +one decode function per scan type). The `Rasterizer` class in `src/render/text.ts` is a translation of font-rs' +`Raster` from Rust to TypeScript (`draw_line`, `draw_quad`, `accumulate`); its `cubic` method is adapted from +`draw_quad`. The full license text is in [licenses/Apache-2.0.txt](licenses/Apache-2.0.txt). diff --git a/bench/_.mjs b/bench/_.mjs deleted file mode 100644 index 91190e7..0000000 --- a/bench/_.mjs +++ /dev/null @@ -1,166 +0,0 @@ -// https://gist.github.com/evanwashere/7ee592870e46f80405b9776dcd56e1e8#file-bench-js - -const now = performance.now.bind(performance); - -function sort(a, b) { - if (a > b) return 1; - if (a < b) return -1; - - return 0; -}; - -function stats(n, avg, min, max, jit, all) { - return { - min: Math.ceil(min * 1e6), - max: Math.ceil(max * 1e6), - avg: Math.ceil(avg / n * 1e6), - jit: jit.map(x => Math.ceil(x * 1e6)), - '50th': Math.ceil(1e6 * all[Math.ceil(n * (50 / 100)) - 1]), - '75th': Math.ceil(1e6 * all[Math.ceil(n * (75 / 100)) - 1]), - '99th': Math.ceil(1e6 * all[Math.ceil(n * (99 / 100)) - 1]), - '99.5th': Math.ceil(1e6 * all[Math.ceil(n * (99.5 / 100)) - 1]), - '99.9th': Math.ceil(1e6 * all[Math.ceil(n * (99.9 / 100)) - 1]), - }; -} - -export function sync(n, fn) { - let avg = 0; - let min = Infinity; - let max = -Infinity; - const all = new Array(n); - const jit = new Array(10); - - warmup: { - let offset = 0; - let iterations = 10; - while (iterations--) { - const t1 = now(); - - fn(); - jit[offset++] = now() - t1; - } - - iterations = 1e3 - 10; - while (iterations--) fn(); - } - - measure: { - let offset = 0; - let iterations = n; - while (iterations--) { - const t1 = now(); - - fn(); - const t2 = now() - t1; - if (t2 < min) min = t2; - if (t2 > max) max = t2; - avg += (all[offset++] = t2); - } - } - - all.sort(sort); - return stats(n, avg, min, max, jit, all); -} - -export async function async(n, fn) { - let avg = 0; - let min = Infinity; - let max = -Infinity; - const all = new Array(n); - const jit = new Array(10); - - warmup: { - let offset = 0; - let iterations = 10; - while (iterations--) { - const t1 = now(); - - await fn(); - jit[offset++] = now() - t1; - } - - iterations = 1e3 - 10; - while (iterations--) await fn(); - } - - measure: { - let offset = 0; - let iterations = n; - while (iterations--) { - const t1 = now(); - - await fn(); - const t2 = now() - t1; - if (t2 < min) min = t2; - if (t2 > max) max = t2; - avg += (all[offset++] = t2); - } - } - - all.sort(sort); - return stats(n, avg, min, max, jit, all); -} - -export function format({ results, title = '', unit = 'ns', percentiles = true }) { - const h = '─'; - const v = '│'; - - let s = ''; - unit = `${unit}/iter`; - const rk = Object.keys(results); - const rv = Object.values(results); - const ra = rv.map(x => x.avg.toLocaleString('en-us')); - const r50 = rv.map(x => x['50th'].toLocaleString('en-us')); - const r75 = rv.map(x => x['75th'].toLocaleString('en-us')); - const r99 = rv.map(x => x['99th'].toLocaleString('en-us')); - const r995 = rv.map(x => x['99.5th'].toLocaleString('en-us')); - const r999 = rv.map(x => x['99.9th'].toLocaleString('en-us')); - const rmm = rv.map(x => [x.min.toLocaleString('en-us'), x.max.toLocaleString('en-us')]); - - const us = unit.length; - const rks = Math.max(...rk.map(x => x.length)); - const ras = Math.max(...ra.map(x => x.length)); - const r50s = Math.max(...r50.map(x => x.length)); - const r75s = Math.max(...r75.map(x => x.length)); - const r99s = Math.max(...r99.map(x => x.length)); - const r995s = Math.max(...r995.map(x => x.length)); - const r999s = Math.max(...r999.map(x => x.length)); - const rmns = Math.max(...rmm.map(x => x[0].length)); - const rmxs = Math.max(...rmm.map(x => x[1].length)); - - const bks = 1 + rks + 1; - const b50s = 1 + r50s + 1 + us + 1; - const b75s = 1 + r75s + 1 + us + 1; - const b99s = 1 + r99s + 1 + us + 1; - const b995s = 1 + r995s + 1 + us + 1; - const b999s = 1 + r999s + 1 + us + 1; - const bimms = 1 + rmns + 2 + rmxs + 1 + us + 1; - const bas = 1 + (ras + 1 + us) + 1 + bimms + 1; - const ls = 1 + bks + 1 + bas + 1 + b50s + 1 + b75s + 1 + b99s + 1 + b995s + 1 + b999s + 1; - - if (!percentiles) s += title ? ` ${title}` : ''; - - else { - s += ' '.repeat(3 + bks + bas); - s += '┌' + h.repeat(ls - 4 - bks - bas) + '┐'; - s += '\n' + ` ${title.padEnd(2 + bks + bas, ' ')}`; - } - - if (percentiles) s += v - + '50th'.padEnd((b50s + 4) / 2, ' ').padStart(b50s, ' ') + v - + '75th'.padEnd((b75s + 4) / 2, ' ').padStart(b75s, ' ') + v - + '99th'.padEnd((b99s + 4) / 2, ' ').padStart(b99s, ' ') + v - + '99.5th'.padEnd((b995s + 6) / 2, ' ').padStart(b995s, ' ') + v - + '99.9th'.padEnd((b999s + 6) / 2, ' ').padStart(b999s, ' ') + v; - - s += (title || percentiles ? '\n' : '') + '┌' + h.repeat(1 + bks + bas) + '┐' + (percentiles ? '├' + h.repeat(ls - 4 - bks - bas) + '┤' : ''); - - for (let i = 0; i < rk.length; i++) { - s += '\n' + v + ` ${rk[i].padEnd(rks, ' ')} ` + v + ` ${ra[i].padStart(ras, ' ')} ${unit} ${`(${rmm[i][0]}..${rmm[i][1]} ${unit})`.padStart(bimms, ' ')} ` + v - if (percentiles) s += v + ` ${r50[i].padStart(r50s, ' ')} ${unit} ` + v + ` ${r75[i].padStart(r75s, ' ')} ${unit} ` + v + ` ${r99[i].padStart(r99s, ' ')} ${unit} ` + v + ` ${r995[i].padStart(r995s, ' ')} ${unit} ` + v + ` ${r999[i].padStart(r999s, ' ')} ${unit} ` + v; - } - - s += '\n' + '└' + h.repeat(1 + bks + bas) + '┘' + (percentiles ? '└' + h.repeat(ls - 4 - bks - bas) + '┘' : ''); - - return s; -} \ No newline at end of file diff --git a/bench/all.mjs b/bench/all.mjs deleted file mode 100644 index 8272727..0000000 --- a/bench/all.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import './flip.mjs'; -import './blur.mjs'; -import './resize.mjs'; -import './overlay.mjs'; \ No newline at end of file diff --git a/bench/blur.mjs b/bench/blur.mjs deleted file mode 100644 index 5211946..0000000 --- a/bench/blur.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import { sync, async, format } from './_.mjs'; - -import jimp from 'jimp'; -import sharp from 'sharp'; -import v2 from '../v2/framebuffer.mjs'; -import * as imagers from '@evan/wasm/target/image/node.mjs'; -import * as v2_wasm from '@evan/wasm/target/imagescript/node.mjs'; - -{ - const rgba = new Uint8Array(4 * 256 * 256).map((_, i) => i % 256); - - const images = { - v2: new v2(256, 256, rgba), - imagers: imagers.framebuffer.from(256, 256, rgba), - v2_wasm: () => v2_wasm.framebuffer.from(256, 256, rgba), - sharp: sharp(rgba, { raw: { width: 256, height: 256, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 256, height: 256, data: rgba }, (_, i) => r(i))), - }; - - console.log(format({ - percentiles: false, - title: 'blur (5.0f/256x256)', - - results: { - jimp: sync(1e4, () => images.jimp.clone().blur(5)), - imagescript: sync(1e4, () => images.v2.clone().blur('gaussian')), - sharp: await async(1e4, () => images.sharp.clone().blur(5).toBuffer()), - 'image-rs (wasm)': sync(1e4, () => imagers.blur(images.imagers, 5).drop()), - 'imagescript-rs (wasm)': sync(1e4, () => { let i = images.v2_wasm(); v2_wasm.blur(i, 5); i.drop() }), - }, - })); - - images.imagers.drop(); -} - -{ - const rgba = new Uint8Array(4 * 1024 * 1024).map((_, i) => i % 256); - - const images = { - v2: new v2(1024, 1024, rgba), - imagers: imagers.framebuffer.from(1024, 1024, rgba), - v2_wasm: () => v2_wasm.framebuffer.from(1024, 1024, rgba), - sharp: sharp(rgba, { raw: { width: 1024, height: 1024, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 1024, height: 1024, data: rgba }, (_, i) => r(i))), - }; - - console.log(format({ - percentiles: false, - title: 'blur (5.0f/1024x1024)', - - results: { - jimp: sync(1e4, () => images.jimp.clone().blur(5)), - imagescript: sync(1e4, () => images.v2.clone().blur('gaussian')), - sharp: await async(1e4, () => images.sharp.clone().blur(5).toBuffer()), - 'image-rs (wasm)': sync(1e4, () => imagers.blur(images.imagers, 5).drop()), - 'imagescript-rs (wasm)': sync(1e4, () => { let i = images.v2_wasm(); v2_wasm.blur(i, 5); i.drop() }), - }, - })); -} \ No newline at end of file diff --git a/bench/flip.mjs b/bench/flip.mjs deleted file mode 100644 index c4eff9b..0000000 --- a/bench/flip.mjs +++ /dev/null @@ -1,63 +0,0 @@ -import { sync, async, format } from './_.mjs'; - -import jimp from 'jimp'; -import sharp from 'sharp'; -import v2 from '../v2/framebuffer.mjs'; -import * as imagers from '@evan/wasm/target/image/node.mjs'; -import * as v2_wasm from '@evan/wasm/target/imagescript/node.mjs'; - -{ - const rgba = new Uint8Array(4 * 256 * 256).map((_, i) => i % 256); - - const images = { - v2: new v2(256, 256, rgba), - imagers: imagers.framebuffer.from(256, 256, rgba), - v2_wasm: v2_wasm.framebuffer.from(256, 256, rgba), - sharp: sharp(rgba, { raw: { width: 256, height: 256, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 256, height: 256, data: rgba }, (_, i) => r(i))), - }; - - console.log(format({ - percentiles: false, - title: 'flip horizontal (256)', - - results: { - jimp: sync(1e4, () => images.jimp.flip(true, false)), - imagescript: sync(1e4, () => images.v2.flip('horizontal')), - sharp: await async(1e4, () => images.sharp.flip().toBuffer()), - 'image-rs (wasm)': sync(1e4, () => images.imagers.flip_horizontal()), - 'imagescript-rs (wasm)': sync(1e4, () => v2_wasm.flip_horizontal(images.v2_wasm)), - }, - })); - - images.v2_wasm.drop(); - images.imagers.drop(); -} - -{ - const rgba = new Uint8Array(4 * 256 * 256).map((_, i) => i % 256); - - const images = { - v2: new v2(256, 256, rgba), - imagers: imagers.framebuffer.from(256, 256, rgba), - v2_wasm: v2_wasm.framebuffer.from(256, 256, rgba), - sharp: sharp(rgba, { raw: { width: 256, height: 256, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 256, height: 256, data: rgba }, (_, i) => r(i))), - }; - - console.log(format({ - percentiles: false, - title: 'flip vertical (256)', - - results: { - jimp: sync(1e4, () => images.jimp.flip(false, true)), - imagescript: sync(1e4, () => images.v2.flip('vertical')), - sharp: await async(1e4, () => images.sharp.flop().toBuffer()), - 'image-rs (wasm)': sync(1e4, () => images.imagers.flip_vertical()), - 'imagescript-rs (wasm)': sync(1e4, () => v2_wasm.flip_vertical(images.v2_wasm)), - }, - })); - - images.v2_wasm.drop(); - images.imagers.drop(); -} \ No newline at end of file diff --git a/bench/overlay.mjs b/bench/overlay.mjs deleted file mode 100644 index ea968fe..0000000 --- a/bench/overlay.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import { sync, async, format } from './_.mjs'; - -import jimp from 'jimp'; -import sharp from 'sharp'; -import v2 from '../v2/framebuffer.mjs'; -import * as imagers from '@evan/wasm/target/image/node.mjs'; -import * as v2_wasm from '@evan/wasm/target/imagescript/node.mjs'; - -{ - const rgba = new Uint8Array(4 * 256 * 256).map((_, i) => i % 256); - - const images = { - v2: new v2(256, 256, rgba), - imagers: imagers.framebuffer.from(256, 256, rgba), - v2_wasm: v2_wasm.framebuffer.from(256, 256, rgba), - sharp: sharp(rgba, { raw: { width: 256, height: 256, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 256, height: 256, data: rgba }, (_, i) => r(i))), - }; - - console.log(format({ - percentiles: false, - title: 'overlay(256x256)', - - results: { - jimp: sync(1e4, () => images.jimp.composite(images.jimp, 0, 0)), - imagescript: sync(1e4, () => images.v2.overlay(images.v2, 0, 0)), - 'image-rs (wasm)': sync(1e4, () => images.imagers.overlay(images.imagers, 0, 0)), - 'imagescript-rs (wasm)': sync(1e4, () => v2_wasm.overlay(images.v2_wasm, images.v2_wasm, 0, 0)), - sharp: await async(1e4, () => images.sharp.composite([{ input: rgba, raw: { width: 256, height: 256, channels: 4 } }]).toBuffer()), - }, - })); - - images.v2_wasm.drop(); - images.imagers.drop(); -} \ No newline at end of file diff --git a/bench/package.json b/bench/package.json deleted file mode 100644 index 9b713a9..0000000 --- a/bench/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "dependencies": { - "jimp": "^0.16.1", - "sharp": "^0.29.2", - "@evan/wasm": "^0.0.92" - } -} \ No newline at end of file diff --git a/bench/readme.md b/bench/readme.md deleted file mode 100644 index de13077..0000000 --- a/bench/readme.md +++ /dev/null @@ -1,64 +0,0 @@ -these benchmarks are purely for v2 progress tracking - -`node all.mjs` -```js - resize linear (256 -> 1024) -┌─────────────────────────────────────────────────────────────────────────────┐ -│ imagescript │ 19,503,902 ns/iter (18,861,626..27,912,168 ns/iter) │ -│ imagescript-rs (wasm) │ 17,713,101 ns/iter (17,476,625..23,760,793 ns/iter) │ -└─────────────────────────────────────────────────────────────────────────────┘ - resize cubic (256 -> 1024) -┌────────────────────────────────────────────────────────────────────────────────┐ -│ imagescript │ 235,246,467 ns/iter (229,790,251..354,907,209 ns/iter) │ -│ sharp │ 14,342,381 ns/iter (10,830,958..39,727,583 ns/iter) │ -│ imagescript-rs (wasm) │ 63,684,581 ns/iter (62,884,708..75,180,751 ns/iter) │ -└────────────────────────────────────────────────────────────────────────────────┘ - flip horizontal (256) -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ jimp │ 1,259,846 ns/iter (1,209,542..2,151,208 ns/iter) │ -│ imagescript │ 26,333 ns/iter (24,626..93,501 ns/iter) │ -│ sharp │ 24,134,341 ns/iter (274,959..210,144,563,500 ns/iter) │ -│ image-rs (wasm) │ 43,797 ns/iter (43,501..74,293 ns/iter) │ -│ imagescript-rs (wasm) │ 14,309 ns/iter (13,959..49,333 ns/iter) │ -└─────────────────────────────────────────────────────────────────────────────────┘ - flip vertical (256) -┌───────────────────────────────────────────────────────────────────────────┐ -│ jimp │ 1,281,557 ns/iter (1,223,292..7,604,917 ns/iter) │ -│ imagescript │ 54,151 ns/iter (53,333..89,959 ns/iter) │ -│ sharp │ 2,552,946 ns/iter (513,959..10,188,876 ns/iter) │ -│ image-rs (wasm) │ 30,475 ns/iter (30,167..88,250 ns/iter) │ -│ imagescript-rs (wasm) │ 13,747 ns/iter (13,542..27,042 ns/iter) │ -└───────────────────────────────────────────────────────────────────────────┘ - overlay(256x256) -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ jimp │ 3,428,386 ns/iter (3,372,335..5,125,459 ns/iter) │ -│ imagescript │ 259,099 ns/iter (256,583..310,334 ns/iter) │ -│ image-rs (wasm) │ 541,678 ns/iter (533,166..1,509,710 ns/iter) │ -│ imagescript-rs (wasm) │ 143,653 ns/iter (137,293..1,242,793 ns/iter) │ -│ sharp │ 28,689,423 ns/iter (1,837,000..210,144,590,042 ns/iter) │ -└─────────────────────────────────────────────────────────────────────────────────┘ - resize nearest (256 -> 1024) -┌───────────────────────────────────────────────────────────────────────────────┐ -│ imagescript │ 1,452,792 ns/iter (1,327,334..13,043,958 ns/iter) │ -│ jimp │ 6,053,008 ns/iter (5,835,042..9,540,835 ns/iter) │ -│ sharp │ 6,209,299 ns/iter (2,089,376..15,344,367,876 ns/iter) │ -│ image-rs (wasm) │ 4,823,582 ns/iter (4,809,042..7,176,000 ns/iter) │ -│ imagescript-rs (wasm) │ 600,103 ns/iter (597,042..802,751 ns/iter) │ -└───────────────────────────────────────────────────────────────────────────────┘ - blur (5.0f/256x256) -┌─────────────────────────────────────────────────────────────────────────────────┐ -│ jimp │ 3,752,613 ns/iter (3,338,751..17,802,125 ns/iter) │ -│ imagescript │ 1,982,010 ns/iter (1,935,459..2,907,833 ns/iter) │ -│ sharp │ 36,079,515 ns/iter (2,465,000..210,155,512,001 ns/iter) │ -│ image-rs (wasm) │ 6,050,020 ns/iter (6,037,125..8,032,876 ns/iter) │ -│ imagescript-rs (wasm) │ 1,288,080 ns/iter (1,263,834..1,699,542 ns/iter) │ -└─────────────────────────────────────────────────────────────────────────────────┘ - blur (5.0f/1024x1024) -┌───────────────────────────────────────────────────────────────────────────────┐ -│ jimp │ 105,250,057 ns/iter (89,942,042..302,817,625 ns/iter) │ -│ imagescript │ 31,996,324 ns/iter (31,372,417..101,801,209 ns/iter) │ -│ sharp │ 20,019,663 ns/iter (18,547,168..66,675,542 ns/iter) │ -│ image-rs (wasm) │ 97,924,025 ns/iter (96,879,584..158,598,250 ns/iter) │ -│ imagescript-rs (wasm) │ 20,960,014 ns/iter (20,533,709..104,136,960 ns/iter) │ -└───────────────────────────────────────────────────────────────────────────────┘ -``` \ No newline at end of file diff --git a/bench/resize.mjs b/bench/resize.mjs deleted file mode 100644 index 2028ce7..0000000 --- a/bench/resize.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import { sync, async, format } from './_.mjs'; - -import jimp from 'jimp'; -import sharp from 'sharp'; -import v2 from '../v2/framebuffer.mjs'; -import * as imagers from '@evan/wasm/target/image/node.mjs'; -import * as v2_wasm from '@evan/wasm/target/imagescript/node.mjs'; -const rgba = new Uint8Array(4 * 256 * 256).map((_, i) => i % 256); - -const images = { - v2: () => new v2(256, 256, rgba), - v2_wasm: v2_wasm.framebuffer.from(256, 256, rgba), - imagers: imagers.framebuffer.from(256, 256, rgba), - sharp: sharp(rgba, { raw: { width: 256, height: 256, channels: 4 } }), - jimp: await new Promise(r => new jimp({ width: 256, height: 256, data: rgba }, (_, i) => r(i))), -}; - -console.log(format({ - percentiles: false, - title: 'resize linear (256 -> 1024)', - - results: { - // others don't have linear - imagescript: sync(1e3, () => images.v2().resize('linear', 1024, 1024)), - 'imagescript-rs (wasm)': sync(1e3, () => v2_wasm.resize(images.v2_wasm, 'linear', 1024, 1024).drop()), - }, -})); - -console.log(format({ - percentiles: false, - title: 'resize cubic (256 -> 1024)', - - results: { - // others don't have cubic - imagescript: sync(1e3, () => images.v2().resize('cubic', 1024, 1024)), - sharp: await async(1e3, () => images.sharp.resize(1024, 1024, { kernel: 'cubic' }).toBuffer()), - 'imagescript-rs (wasm)': sync(1e3, () => v2_wasm.resize(images.v2_wasm, 'cubic', 1024, 1024).drop()), - }, -})); - -console.log(format({ - percentiles: false, - title: 'resize nearest (256 -> 1024)', - - results: { - imagescript: sync(1e4, () => images.v2().resize('nearest', 1024, 1024)), - jimp: sync(1e4, () => images.jimp.resize(1024, 1024, jimp.RESIZE_NEAREST_NEIGHBOR)), - sharp: await async(1e4, () => images.sharp.resize(1024, 1024, { kernel: 'nearest' }).toBuffer()), - 'image-rs (wasm)': sync(1e4, () => imagers.resize(images.imagers, 'nearest', 1024, 1024).drop()), - 'imagescript-rs (wasm)': sync(1e4, () => v2_wasm.resize(images.v2_wasm, 'nearest', 1024, 1024).drop()), - }, -})); \ No newline at end of file diff --git a/codecs/.npmignore b/codecs/.npmignore deleted file mode 100644 index eb5b7ff..0000000 --- a/codecs/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -src/ -test/ \ No newline at end of file diff --git a/codecs/LICENSE b/codecs/LICENSE deleted file mode 100644 index c878a6c..0000000 --- a/codecs/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021-present matmen, evanwashere - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/codecs/lib.d.ts b/codecs/lib.d.ts deleted file mode 100644 index df9d05b..0000000 --- a/codecs/lib.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -declare module '@imagescript/codecs' { - type wah = { width: number, height: number }; - type jpeg_encode_options = wah & { quality?: number }; - type webp_encode_options = wah & { quality?: number }; - - type png_encode_options = wah & { - compression?: 'fast' | 'best' | 'default', - filter?: 'up' | 'sub' | 'none' | 'paeth' | 'average', - }; - - type gif_frame_options = wah & { - x?: number, - y?: number, - delay?: number, - speed?: number, - colors?: number, - quality?: number, - dispose?: 'any' | 'keep' | 'previous' | 'background', - } - - - class gif_encoder { - width: number; - height: number; - - constructor(width: number, height: number): this; - add(buffer: ArrayBufferView, options: gif_frame_options): void; - finish(options: { repeat?: number, comment: string, application?: string }): Uint8Array; - } - - - export interface gif { - encoder: gif_encoder, - } - - export interface png { - encode(buffer: ArrayBufferView, options: png_encode_options): Uint8Array, - encode_async(buffer: ArrayBufferView, options: png_encode_options): Promise, - } - - export interface jpeg { - encode(buffer: ArrayBufferView, options: png_encode_options): Uint8Array, - encode_async(buffer: ArrayBufferView, options: png_encode_options): Promise, - } - - export interface webp { - encode(buffer: ArrayBufferView, options: png_encode_options): Uint8Array, - encode_async(buffer: ArrayBufferView, options: png_encode_options): Promise, - } -} \ No newline at end of file diff --git a/codecs/lib.js b/codecs/lib.js deleted file mode 100644 index 38d0592..0000000 --- a/codecs/lib.js +++ /dev/null @@ -1,6 +0,0 @@ -let codecs; - -if (process.env.CODECS_FORCE_WASM) codecs = require('./wasm/index.js'); -else try { codecs = require('./node/index.js'); } catch { codecs = require('./wasm/index.js'); } - -throw new Error('todo!'); \ No newline at end of file diff --git a/codecs/lib.mjs b/codecs/lib.mjs deleted file mode 100644 index e270c4a..0000000 --- a/codecs/lib.mjs +++ /dev/null @@ -1 +0,0 @@ -import lib from './lib.js'; \ No newline at end of file diff --git a/codecs/node/bin/arm64-darwin.node b/codecs/node/bin/arm64-darwin.node deleted file mode 100755 index b3d6509..0000000 Binary files a/codecs/node/bin/arm64-darwin.node and /dev/null differ diff --git a/codecs/node/bin/arm64-linux.node b/codecs/node/bin/arm64-linux.node deleted file mode 100755 index cb8df84..0000000 Binary files a/codecs/node/bin/arm64-linux.node and /dev/null differ diff --git a/codecs/node/bin/x64-darwin.node b/codecs/node/bin/x64-darwin.node deleted file mode 100755 index 2a2e73c..0000000 Binary files a/codecs/node/bin/x64-darwin.node and /dev/null differ diff --git a/codecs/node/bin/x64-linux.node b/codecs/node/bin/x64-linux.node deleted file mode 100755 index a6d6068..0000000 Binary files a/codecs/node/bin/x64-linux.node and /dev/null differ diff --git a/codecs/node/bin/x64-win32.node b/codecs/node/bin/x64-win32.node deleted file mode 100644 index b711e52..0000000 Binary files a/codecs/node/bin/x64-win32.node and /dev/null differ diff --git a/codecs/node/index.js b/codecs/node/index.js deleted file mode 100644 index af92a81..0000000 --- a/codecs/node/index.js +++ /dev/null @@ -1,3 +0,0 @@ -const { arch, platform } = require('os'); -try { module.exports = require(`./bin/${arch()}-${platform()}.node`); } -catch (err) { throw new Error('unsupported arch/platform: ' + err.message); } \ No newline at end of file diff --git a/codecs/node/index.mjs b/codecs/node/index.mjs deleted file mode 100644 index 698ce73..0000000 --- a/codecs/node/index.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import lib from './index.js'; - -export const png = lib.png; -export const gif = lib.gif; -export const jpeg = lib.jpeg; -export const webp = lib.webp; \ No newline at end of file diff --git a/codecs/package.json b/codecs/package.json deleted file mode 100644 index af14301..0000000 --- a/codecs/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "license": "MIT", - "main": "lib.js", - "version": "0.0.1", - "types": "lib.d.ts", - "name": "@imagescript/codecs", - "description": "fast codecs bindings for node and browsers", - "author": "imagescript (https://github.com/matmen/imagescript)", - "bugs": { "url": "https://github.com/matmen/imagescript/issues" }, - "homepage": "https://github.com/matmen/ImageScript/blob/master/codecs/readme.md", - "repository": { "type": "git", "url": "git+https://github.com/matmen/imagescript.git" }, - "exports": { ".": [{ "import": "./lib.mjs", "require": "./lib.js" }, "./lib.js"], "./*": "./*" }, - "keywords": ["png", "jpg", "gif", "jpeg", "webp", "fast", "wasm", "codecs", "native", "bindings"] -} \ No newline at end of file diff --git a/codecs/readme.md b/codecs/readme.md deleted file mode 100644 index 4e8a4aa..0000000 --- a/codecs/readme.md +++ /dev/null @@ -1,38 +0,0 @@ -# fast codecs bindings for node and browsers - -`npm i @imagescript/codecs` - -```js -import { png } from '@imagescript/codecs'; - -const rgba = new Uint32Array(256 * 256); -const image = png.encode(rgba, { width: 256, height: 256 }); -``` - -## supported platforms -| | node@10 | node@12 | node@14 | node@16 | -| ---------------- | ------- | ------- | ------- | ------- | -| wasm32 | ✕ | ✕ | ✕ | ✕ | -| macos x64 | ✓ | ✓ | ✓ | ✓ | -| macos arm64 | ✓ | ✓ | ✓ | ✓ | -| windows x64 | ✓ | ✓ | ✓ | ✓ | -| linux x64 gnu | ✓ | ✓ | ✓ | ✓ | -| linux arm64 gnu | ✓ | ✓ | ✓ | ✓ | - -you can force usage of wasm by setting `CODECS_FORCE_WASM` env variable - -for deno and browsers use `@imagescript/codecs/wasm/bundle/browser.js` [cdn](https://unpkg.com/@imagescript/codecs/wasm/bundle/browser.js) - -## benchmarks - -image used for benchmarks: `new framebuffer(width, height).fill((x, y) => x * y)` - -![benchmark](https://plot.evan.lol/bar/eyJ0aXRsZSI6IkdJRiBvcHMvcyAoaGlnaGVyIGlzIGJldHRlcikiLCJwb2ludHMiOlt7Im5hbWUiOiJlbmNvZGUoNXggUkdCQSAyNTZ4MjU2KSIsInNjb3JlcyI6WzE0LjU2LDIuMzZdfSx7Im5hbWUiOiJlbmNvZGUoNXggUkdCQSAxMDI0eDEwMjQpIiwic2NvcmVzIjpbMC43MywwLjIxXX1dLCJsZWdlbmQiOlt7Im5hbWUiOiJuYXRpdmUoQGltYWdlc2NyaXB0L2NvZGVjcykiLCJjb2xvciI6ODUxMTM4MDQ3fSx7Im5hbWUiOiJqcyhAc2t5cmEvZ2lmZW5jKSIsImNvbG9yIjotMTI1MDk3MjY3M31dfQ==.png) - -![benchmark](https://plot.evan.lol/bar/eyJ0aXRsZSI6IkpQRUcgb3BzL3MgKGhpZ2hlciBpcyBiZXR0ZXIpIiwicG9pbnRzIjpbeyJuYW1lIjoiZW5jb2RlKFJHQkEgNjR4NjQpIiwic2NvcmVzIjpbNDMuNTgsNTcyOS4wOCw4MTEuMjldfSx7Im5hbWUiOiJlbmNvZGUoUkdCQSAyNTZ4MjU2KSIsInNjb3JlcyI6WzMyLjQ0LDM2NS45MywyMjguN119LHsibmFtZSI6ImVuY29kZShSR0JBIDUxMng1MTIpIiwic2NvcmVzIjpbMTYuMjIsODIuOTgsNzkuMjddfSx7Im5hbWUiOiJlbmNvZGUoUkdCQSAxMDI0eDEwMjQpIiwic2NvcmVzIjpbNC4yMiwxOS4xNiwyMi43OV19LHsibmFtZSI6ImVuY29kZShSR0JBIDIwNDh4MjA0OCkiLCJzY29yZXMiOlsxLjA1LDQuODksNS42N119XSwibGVnZW5kIjpbeyJuYW1lIjoianMoanBlZy1qcykiLCJjb2xvciI6ODUxMTM4MDQ3fSx7Im5hbWUiOiJuYXRpdmUoQGltYWdlc2NyaXB0L2NvZGVjcykiLCJjb2xvciI6LTEyNTA5NzI2NzN9LHsibmFtZSI6Im5hdGl2ZShzaGFycCkiLCJjb2xvciI6LTExNTY1MDA0ODF9XX0=.png) - -![benchmark](https://plot.evan.lol/bar/eyJ0aXRsZSI6IlBORyBvcHMvcyAoaGlnaGVyIGlzIGJldHRlcikiLCJwb2ludHMiOlt7Im5hbWUiOiJlbmNvZGUoUkdCQSA2NHg2NCkiLCJzY29yZXMiOlsyNDAuNzEsMTQzNy44Nyw2OTAuOTQsNzY2LjA1XX0seyJuYW1lIjoiZW5jb2RlKFJHQkEgMjU2eDI1NikiLCJzY29yZXMiOlsxMi43OSw1Ni4yOCwzMS41MywxMTIuNDZdfSx7Im5hbWUiOiJlbmNvZGUoUkdCQSA1MTJ4NTEyKSIsInNjb3JlcyI6WzkuOTQsMjUuODMsMTAuMzEsMzQuODZdfSx7Im5hbWUiOiJlbmNvZGUoUkdCQSAxMDI0eDEwMjQpIiwic2NvcmVzIjpbMy4wNCw3LDMuMzcsMTAuMzddfSx7Im5hbWUiOiJlbmNvZGUoUkdCQSAyMDQ4eDIwNDgpIiwic2NvcmVzIjpbMC44LDEuNzYsMC45NywyLjddfV0sImxlZ2VuZCI6W3sibmFtZSI6ImpzKHVwbmcpIiwiY29sb3IiOjg1MTEzODA0N30seyJuYW1lIjoibmF0aXZlKEBpbWFnZXNjcmlwdC9jb2RlY3MpIiwiY29sb3IiOi0xMjUwOTcyNjczfSx7Im5hbWUiOiJqcyhpbWFnZXNjcmlwdCkiLCJjb2xvciI6LTExNTY1MDA0ODF9LHsibmFtZSI6Im5hdGl2ZShzaGFycCkiLCJjb2xvciI6LTE0MTI4NzE2OX1dfQ==.png) - -## License - -MIT © [evan](https://github.com/evanwashere) [matmen](https://github.com/matmen) \ No newline at end of file diff --git a/codecs/tests/gif.js b/codecs/tests/gif.js deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/tests/jpeg.js b/codecs/tests/jpeg.js deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/tests/png.js b/codecs/tests/png.js deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/tests/results/gif/1024.nis.gif b/codecs/tests/results/gif/1024.nis.gif deleted file mode 100644 index 9466ffa..0000000 Binary files a/codecs/tests/results/gif/1024.nis.gif and /dev/null differ diff --git a/codecs/tests/results/gif/1024.skyra.gif b/codecs/tests/results/gif/1024.skyra.gif deleted file mode 100644 index de381e8..0000000 Binary files a/codecs/tests/results/gif/1024.skyra.gif and /dev/null differ diff --git a/codecs/tests/results/gif/256.nis.gif b/codecs/tests/results/gif/256.nis.gif deleted file mode 100644 index 9467854..0000000 Binary files a/codecs/tests/results/gif/256.nis.gif and /dev/null differ diff --git a/codecs/tests/results/gif/256.skyra.gif b/codecs/tests/results/gif/256.skyra.gif deleted file mode 100644 index 4e8ac9c..0000000 Binary files a/codecs/tests/results/gif/256.skyra.gif and /dev/null differ diff --git a/codecs/tests/results/jpeg/256.jpegjs.jpeg b/codecs/tests/results/jpeg/256.jpegjs.jpeg deleted file mode 100644 index 7cd8f2b..0000000 Binary files a/codecs/tests/results/jpeg/256.jpegjs.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/256.nis.jpeg b/codecs/tests/results/jpeg/256.nis.jpeg deleted file mode 100644 index e6bb878..0000000 Binary files a/codecs/tests/results/jpeg/256.nis.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/256.sharp.jpeg b/codecs/tests/results/jpeg/256.sharp.jpeg deleted file mode 100644 index d57978c..0000000 Binary files a/codecs/tests/results/jpeg/256.sharp.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/512.jpegjs.jpeg b/codecs/tests/results/jpeg/512.jpegjs.jpeg deleted file mode 100644 index e87be33..0000000 Binary files a/codecs/tests/results/jpeg/512.jpegjs.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/512.nis.jpeg b/codecs/tests/results/jpeg/512.nis.jpeg deleted file mode 100644 index 5f52078..0000000 Binary files a/codecs/tests/results/jpeg/512.nis.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/512.sharp.jpeg b/codecs/tests/results/jpeg/512.sharp.jpeg deleted file mode 100644 index 9a2c269..0000000 Binary files a/codecs/tests/results/jpeg/512.sharp.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/64.jpegjs.jpeg b/codecs/tests/results/jpeg/64.jpegjs.jpeg deleted file mode 100644 index a3910bb..0000000 Binary files a/codecs/tests/results/jpeg/64.jpegjs.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/64.nis.jpeg b/codecs/tests/results/jpeg/64.nis.jpeg deleted file mode 100644 index 9eb85c9..0000000 Binary files a/codecs/tests/results/jpeg/64.nis.jpeg and /dev/null differ diff --git a/codecs/tests/results/jpeg/64.sharp.jpeg b/codecs/tests/results/jpeg/64.sharp.jpeg deleted file mode 100644 index e275b71..0000000 Binary files a/codecs/tests/results/jpeg/64.sharp.jpeg and /dev/null differ diff --git a/codecs/tests/results/png/256.jis.png b/codecs/tests/results/png/256.jis.png deleted file mode 100644 index 74358f7..0000000 Binary files a/codecs/tests/results/png/256.jis.png and /dev/null differ diff --git a/codecs/tests/results/png/256.nis.png b/codecs/tests/results/png/256.nis.png deleted file mode 100644 index f9cf386..0000000 Binary files a/codecs/tests/results/png/256.nis.png and /dev/null differ diff --git a/codecs/tests/results/png/256.sharp.png b/codecs/tests/results/png/256.sharp.png deleted file mode 100644 index 7beee04..0000000 Binary files a/codecs/tests/results/png/256.sharp.png and /dev/null differ diff --git a/codecs/tests/results/png/256.upng.png b/codecs/tests/results/png/256.upng.png deleted file mode 100644 index 8009676..0000000 Binary files a/codecs/tests/results/png/256.upng.png and /dev/null differ diff --git a/codecs/tests/results/png/512.jis.png b/codecs/tests/results/png/512.jis.png deleted file mode 100644 index 5589be9..0000000 Binary files a/codecs/tests/results/png/512.jis.png and /dev/null differ diff --git a/codecs/tests/results/png/512.nis.png b/codecs/tests/results/png/512.nis.png deleted file mode 100644 index 76da3c4..0000000 Binary files a/codecs/tests/results/png/512.nis.png and /dev/null differ diff --git a/codecs/tests/results/png/512.sharp.png b/codecs/tests/results/png/512.sharp.png deleted file mode 100644 index c1cf863..0000000 Binary files a/codecs/tests/results/png/512.sharp.png and /dev/null differ diff --git a/codecs/tests/results/png/512.upng.png b/codecs/tests/results/png/512.upng.png deleted file mode 100644 index 72c7d39..0000000 Binary files a/codecs/tests/results/png/512.upng.png and /dev/null differ diff --git a/codecs/tests/results/png/64.jis.png b/codecs/tests/results/png/64.jis.png deleted file mode 100644 index 69f1c7d..0000000 Binary files a/codecs/tests/results/png/64.jis.png and /dev/null differ diff --git a/codecs/tests/results/png/64.nis.png b/codecs/tests/results/png/64.nis.png deleted file mode 100644 index fb82edd..0000000 Binary files a/codecs/tests/results/png/64.nis.png and /dev/null differ diff --git a/codecs/tests/results/png/64.sharp.png b/codecs/tests/results/png/64.sharp.png deleted file mode 100644 index 72066d2..0000000 Binary files a/codecs/tests/results/png/64.sharp.png and /dev/null differ diff --git a/codecs/tests/results/png/64.upng.png b/codecs/tests/results/png/64.upng.png deleted file mode 100644 index 12b64c1..0000000 Binary files a/codecs/tests/results/png/64.upng.png and /dev/null differ diff --git a/codecs/tests/webp.js b/codecs/tests/webp.js deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/wasm/bin/codecs.wasm b/codecs/wasm/bin/codecs.wasm deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/wasm/bin/simd.wasm b/codecs/wasm/bin/simd.wasm deleted file mode 100644 index e69de29..0000000 diff --git a/codecs/wasm/bundle/browser.js b/codecs/wasm/bundle/browser.js deleted file mode 100644 index dd71b68..0000000 --- a/codecs/wasm/bundle/browser.js +++ /dev/null @@ -1 +0,0 @@ -throw new Error('todo!'); \ No newline at end of file diff --git a/codecs/wasm/index.js b/codecs/wasm/index.js deleted file mode 100644 index dd71b68..0000000 --- a/codecs/wasm/index.js +++ /dev/null @@ -1 +0,0 @@ -throw new Error('todo!'); \ No newline at end of file diff --git a/codecs/wasm/index.mjs b/codecs/wasm/index.mjs deleted file mode 100644 index 1a58364..0000000 --- a/codecs/wasm/index.mjs +++ /dev/null @@ -1 +0,0 @@ -import lib from './index.js'; \ No newline at end of file diff --git a/jsr.json b/jsr.json new file mode 100644 index 0000000..ca04ece --- /dev/null +++ b/jsr.json @@ -0,0 +1,9 @@ +{ + "name": "@matmen/imagescript", + "version": "3.0.0", + "license": "(AGPL-3.0-or-later OR MIT)", + "exports": "./src/index.ts", + "publish": { + "include": ["src", "README.md", "LICENSE", "LICENSE.MIT", "LICENSE.AGPL-3.0", "THIRD_PARTY_NOTICES.md", "licenses"] + } +} diff --git a/licenses/Apache-2.0.txt b/licenses/Apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/licenses/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/README.ijg b/licenses/README.ijg new file mode 100644 index 0000000..dbf8070 --- /dev/null +++ b/licenses/README.ijg @@ -0,0 +1,260 @@ +libjpeg-turbo note: This file has been modified by The libjpeg-turbo Project +to include only information relevant to libjpeg-turbo, to wordsmith certain +sections, and to remove impolitic language that existed in the libjpeg v8 +README. It is included only for reference. Please see README.md for +information specific to libjpeg-turbo. + + +The Independent JPEG Group's JPEG software +========================================== + +This distribution contains a release of the Independent JPEG Group's free JPEG +software. You are welcome to redistribute this software and to use it for any +purpose, subject to the conditions under LEGAL ISSUES, below. + +This software is the work of Tom Lane, Guido Vollbeding, Philip Gladstone, +Bill Allombert, Jim Boucher, Lee Crocker, Bob Friesenhahn, Ben Jackson, +Julian Minguillon, Luis Ortiz, George Phillips, Davide Rossi, Ge' Weijers, +and other members of the Independent JPEG Group. + +IJG is not affiliated with the ISO/IEC JTC1/SC29/WG1 standards committee +(also known as JPEG, together with ITU-T SG16). + + +DOCUMENTATION ROADMAP +===================== + +This file contains the following sections: + +OVERVIEW General description of JPEG and the IJG software. +LEGAL ISSUES Copyright, lack of warranty, terms of distribution. +REFERENCES Where to learn more about JPEG. +ARCHIVE LOCATIONS Where to find newer versions of this software. +FILE FORMAT WARS Software *not* to get. +TO DO Plans for future IJG releases. + +Other documentation files in the distribution are: + +User documentation: + doc/usage.txt Usage instructions for cjpeg, djpeg, jpegtran, + rdjpgcom, and wrjpgcom. + doc/*.1 Unix-style man pages for programs (same info as + usage.txt). + doc/wizard.txt Advanced usage instructions for JPEG wizards only. + doc/change.log Version-to-version change highlights. +Programmer and internal documentation: + doc/libjpeg.txt How to use the JPEG library in your own programs. + src/example.c Sample code for calling the JPEG library. + doc/structure.txt Overview of the JPEG library's internal structure. + doc/coderules.txt Coding style rules --- please read if you contribute + code. + +Please read at least usage.txt. Some information can also be found in the JPEG +FAQ (Frequently Asked Questions) article. See ARCHIVE LOCATIONS below to find +out where to obtain the FAQ article. + +If you want to understand how the JPEG code works, we suggest reading one or +more of the REFERENCES, then looking at the documentation files (in roughly +the order listed) before diving into the code. + + +OVERVIEW +======== + +This package contains C software to implement JPEG image encoding, decoding, +and transcoding. JPEG (pronounced "jay-peg") is a standardized compression +method for full-color and grayscale images. JPEG's strong suit is compressing +photographic images or other types of images that have smooth color and +brightness transitions between neighboring pixels. Images with sharp lines or +other abrupt features may not compress well with JPEG, and a higher JPEG +quality may have to be used to avoid visible compression artifacts with such +images. + +JPEG is normally lossy, meaning that the output pixels are not necessarily +identical to the input pixels. However, on photographic content and other +"smooth" images, very good compression ratios can be obtained with no visible +compression artifacts, and extremely high compression ratios are possible if +you are willing to sacrifice image quality (by reducing the "quality" setting +in the compressor.) + +This software implements JPEG baseline, extended-sequential, progressive, and +lossless compression processes. Provision is made for supporting all variants +of these processes, although some uncommon parameter settings aren't +implemented yet. We have made no provision for supporting the hierarchical +processes defined in the standard. + +We provide a set of library routines for reading and writing JPEG image files, +plus two sample applications "cjpeg" and "djpeg", which use the library to +perform conversion between JPEG and some other popular image file formats. +The library is intended to be reused in other applications. + +In order to support file conversion and viewing software, we have included +considerable functionality beyond the bare JPEG coding/decoding capability; +for example, the color quantization modules are not strictly part of JPEG +decoding, but they are essential for output to colormapped file formats. These +extra functions can be compiled out of the library if not required for a +particular application. + +We have also included "jpegtran", a utility for lossless transcoding between +different JPEG processes, and "rdjpgcom" and "wrjpgcom", two simple +applications for inserting and extracting textual comments in JFIF files. + +The emphasis in designing this software has been on achieving portability and +flexibility, while also making it fast enough to be useful. In particular, +the software is not intended to be read as a tutorial on JPEG. (See the +REFERENCES section for introductory material.) Rather, it is intended to +be reliable, portable, industrial-strength code. We do not claim to have +achieved that goal in every aspect of the software, but we strive for it. + +We welcome the use of this software as a component of commercial products. +No royalty is required, but we do ask for an acknowledgement in product +documentation, as described under LEGAL ISSUES. + + +LEGAL ISSUES +============ + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +REFERENCES +========== + +We recommend reading one or more of these references before trying to +understand the innards of the JPEG software. + +The best short technical introduction to the JPEG compression algorithm is + Wallace, Gregory K. "The JPEG Still Picture Compression Standard", + Communications of the ACM, April 1991 (vol. 34 no. 4), pp. 30-44. +(Adjacent articles in that issue discuss MPEG motion picture compression, +applications of JPEG, and related topics.) If you don't have the CACM issue +handy, a PDF file containing a revised version of Wallace's article is +available at http://www.ijg.org/files/Wallace.JPEG.pdf. The file (actually +a preprint for an article that appeared in IEEE Trans. Consumer Electronics) +omits the sample images that appeared in CACM, but it includes corrections +and some added material. Note: the Wallace article is copyright ACM and IEEE, +and it may not be used for commercial purposes. + +A somewhat less technical, more leisurely introduction to JPEG can be found in +"The Data Compression Book" by Mark Nelson and Jean-loup Gailly, published by +M&T Books (New York), 2nd ed. 1996, ISBN 1-55851-434-1. This book provides +good explanations and example C code for a multitude of compression methods +including JPEG. It is an excellent source if you are comfortable reading C +code but don't know much about data compression in general. The book's JPEG +sample code is far from industrial-strength, but when you are ready to look +at a full implementation, you've got one here... + +The best currently available description of JPEG is the textbook "JPEG Still +Image Data Compression Standard" by William B. Pennebaker and Joan L. +Mitchell, published by Van Nostrand Reinhold, 1993, ISBN 0-442-01272-1. +Price US$59.95, 638 pp. The book includes the complete text of the ISO JPEG +standards (DIS 10918-1 and draft DIS 10918-2). + +The original JPEG standard is divided into two parts, Part 1 being the actual +specification, while Part 2 covers compliance testing methods. Part 1 is +titled "Digital Compression and Coding of Continuous-tone Still Images, +Part 1: Requirements and guidelines" and has document numbers ISO/IEC IS +10918-1, ITU-T T.81. Part 2 is titled "Digital Compression and Coding of +Continuous-tone Still Images, Part 2: Compliance testing" and has document +numbers ISO/IEC IS 10918-2, ITU-T T.83. + +The JPEG standard does not specify all details of an interchangeable file +format. For the omitted details, we follow the "JFIF" conventions, revision +1.02. JFIF version 1 has been adopted as ISO/IEC 10918-5 (05/2013) and +Recommendation ITU-T T.871 (05/2011): Information technology - Digital +compression and coding of continuous-tone still images: JPEG File Interchange +Format (JFIF). It is available as a free download in PDF file format from +https://www.iso.org/standard/54989.html and http://www.itu.int/rec/T-REC-T.871. +A PDF file of the older JFIF 1.02 specification is available at +http://www.w3.org/Graphics/JPEG/jfif3.pdf. + +The TIFF 6.0 file format specification can be obtained from +http://mirrors.ctan.org/graphics/tiff/TIFF6.ps.gz. The JPEG incorporation +scheme found in the TIFF 6.0 spec of 3-June-92 has a number of serious +problems. IJG does not recommend use of the TIFF 6.0 design (TIFF Compression +tag 6). Instead, we recommend the JPEG design proposed by TIFF Technical Note +#2 (Compression tag 7). Copies of this Note can be obtained from +http://www.ijg.org/files/. It is expected that the next revision +of the TIFF spec will replace the 6.0 JPEG design with the Note's design. +Although IJG's own code does not support TIFF/JPEG, the free libtiff library +uses our library to implement TIFF/JPEG per the Note. + + +ARCHIVE LOCATIONS +================= + +The "official" archive site for this software is www.ijg.org. +The most recent released version can always be found there in +directory "files". + +The JPEG FAQ (Frequently Asked Questions) article is a source of some +general information about JPEG. It is available at +http://www.faqs.org/faqs/jpeg-faq. + + +FILE FORMAT COMPATIBILITY +========================= + +This software implements ITU T.81 | ISO/IEC 10918 with some extensions from +ITU T.871 | ISO/IEC 10918-5 (JPEG File Interchange Format-- see REFERENCES). +Informally, the term "JPEG image" or "JPEG file" most often refers to JFIF or +a subset thereof, but there are other formats containing the name "JPEG" that +are incompatible with the original JPEG standard or with JFIF (for instance, +JPEG 2000 and JPEG XR). This software therefore does not support these +formats. Indeed, one of the original reasons for developing this free software +was to help force convergence on a common, interoperable format standard for +JPEG files. + +JFIF is a minimal or "low end" representation. TIFF/JPEG (TIFF revision 6.0 as +modified by TIFF Technical Note #2) can be used for "high end" applications +that need to record a lot of additional data about an image. + + +TO DO +===== + +Please send bug reports, offers of help, etc. to jpeg-info@jpegclub.org. diff --git a/package.json b/package.json index bc332b3..1286685 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,33 @@ { "name": "imagescript", - "version": "1.3.1", + "version": "3.0.0", "description": "zero-dependency javascript image manipulation", - "main": "ImageScript.js", - "types": "ImageScript.d.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "LICENSE", + "LICENSE.MIT", + "LICENSE.AGPL-3.0", + "THIRD_PARTY_NOTICES.md", + "licenses", + "README.md" + ], + "sideEffects": false, "scripts": { - "test": "node ./tests/run.js", - "coverage": "nyc --reporter=html npm test" + "build": "node scripts/build.js", + "typecheck": "tsc -p tsconfig.json --noEmit", + "docs": "node scripts/docs.js", + "test": "npm run build && node ./tests/run.js", + "prepack": "npm run build", + "version": "node scripts/build.js && git add src/version.ts jsr.json" }, "repository": { "type": "git", @@ -22,7 +43,6 @@ "scale", "resize", "crop", - "webp", "svg", "bitmap", "gif", @@ -36,6 +56,10 @@ }, "homepage": "https://github.com/matmen/ImageScript#readme", "engines": { - "node": ">=14.0.0" + "node": ">=22.13.0" + }, + "devDependencies": { + "typedoc": "0.27.9", + "typescript": "5.7.3" } } diff --git a/png/README.md b/png/README.md deleted file mode 100644 index 2fd9f95..0000000 --- a/png/README.md +++ /dev/null @@ -1 +0,0 @@ -TBD \ No newline at end of file diff --git a/png/node.js b/png/node.js deleted file mode 100644 index 15bc565..0000000 --- a/png/node.js +++ /dev/null @@ -1,1137 +0,0 @@ -var __defProp = Object.defineProperty; -var __markAsModule = (target) => __defProp(target, "__esModule", {value: true}); -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, {get: all[name], enumerable: true}); -}; - -// ../../Users/evan/GitHub/ImageScript/png/src/png.js -__markAsModule(exports); -__export(exports, { - decode: () => decode, - encode: () => encode -}); - -// ../../Users/evan/GitHub/ImageScript/png/src/crc.js -var table = new Uint32Array([ - 0, - 1996959894, - 3993919788, - 2567524794, - 124634137, - 1886057615, - 3915621685, - 2657392035, - 249268274, - 2044508324, - 3772115230, - 2547177864, - 162941995, - 2125561021, - 3887607047, - 2428444049, - 498536548, - 1789927666, - 4089016648, - 2227061214, - 450548861, - 1843258603, - 4107580753, - 2211677639, - 325883990, - 1684777152, - 4251122042, - 2321926636, - 335633487, - 1661365465, - 4195302755, - 2366115317, - 997073096, - 1281953886, - 3579855332, - 2724688242, - 1006888145, - 1258607687, - 3524101629, - 2768942443, - 901097722, - 1119000684, - 3686517206, - 2898065728, - 853044451, - 1172266101, - 3705015759, - 2882616665, - 651767980, - 1373503546, - 3369554304, - 3218104598, - 565507253, - 1454621731, - 3485111705, - 3099436303, - 671266974, - 1594198024, - 3322730930, - 2970347812, - 795835527, - 1483230225, - 3244367275, - 3060149565, - 1994146192, - 31158534, - 2563907772, - 4023717930, - 1907459465, - 112637215, - 2680153253, - 3904427059, - 2013776290, - 251722036, - 2517215374, - 3775830040, - 2137656763, - 141376813, - 2439277719, - 3865271297, - 1802195444, - 476864866, - 2238001368, - 4066508878, - 1812370925, - 453092731, - 2181625025, - 4111451223, - 1706088902, - 314042704, - 2344532202, - 4240017532, - 1658658271, - 366619977, - 2362670323, - 4224994405, - 1303535960, - 984961486, - 2747007092, - 3569037538, - 1256170817, - 1037604311, - 2765210733, - 3554079995, - 1131014506, - 879679996, - 2909243462, - 3663771856, - 1141124467, - 855842277, - 2852801631, - 3708648649, - 1342533948, - 654459306, - 3188396048, - 3373015174, - 1466479909, - 544179635, - 3110523913, - 3462522015, - 1591671054, - 702138776, - 2966460450, - 3352799412, - 1504918807, - 783551873, - 3082640443, - 3233442989, - 3988292384, - 2596254646, - 62317068, - 1957810842, - 3939845945, - 2647816111, - 81470997, - 1943803523, - 3814918930, - 2489596804, - 225274430, - 2053790376, - 3826175755, - 2466906013, - 167816743, - 2097651377, - 4027552580, - 2265490386, - 503444072, - 1762050814, - 4150417245, - 2154129355, - 426522225, - 1852507879, - 4275313526, - 2312317920, - 282753626, - 1742555852, - 4189708143, - 2394877945, - 397917763, - 1622183637, - 3604390888, - 2714866558, - 953729732, - 1340076626, - 3518719985, - 2797360999, - 1068828381, - 1219638859, - 3624741850, - 2936675148, - 906185462, - 1090812512, - 3747672003, - 2825379669, - 829329135, - 1181335161, - 3412177804, - 3160834842, - 628085408, - 1382605366, - 3423369109, - 3138078467, - 570562233, - 1426400815, - 3317316542, - 2998733608, - 733239954, - 1555261956, - 3268935591, - 3050360625, - 752459403, - 1541320221, - 2607071920, - 3965973030, - 1969922972, - 40735498, - 2617837225, - 3943577151, - 1913087877, - 83908371, - 2512341634, - 3803740692, - 2075208622, - 213261112, - 2463272603, - 3855990285, - 2094854071, - 198958881, - 2262029012, - 4057260610, - 1759359992, - 534414190, - 2176718541, - 4139329115, - 1873836001, - 414664567, - 2282248934, - 4279200368, - 1711684554, - 285281116, - 2405801727, - 4167216745, - 1634467795, - 376229701, - 2685067896, - 3608007406, - 1308918612, - 956543938, - 2808555105, - 3495958263, - 1231636301, - 1047427035, - 2932959818, - 3654703836, - 1088359270, - 936918e3, - 2847714899, - 3736837829, - 1202900863, - 817233897, - 3183342108, - 3401237130, - 1404277552, - 615818150, - 3134207493, - 3453421203, - 1423857449, - 601450431, - 3009837614, - 3294710456, - 1567103746, - 711928724, - 3020668471, - 3272380065, - 1510334235, - 755167117 -]); -function crc32(buffer) { - let offset = 0 | 0; - let crc = 4294967295 | 0; - const bl = buffer.length - 4 | 0; - while (bl > offset) { - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - } - while (offset < buffer.length) { - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - } - return (crc ^ 4294967295) >>> 0; -} - -// ../../Users/evan/GitHub/ImageScript/png/src/mem.js -function view(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) - return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) - return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} -function from_parts(buffers, shared = false) { - let length = 0; - let offset = 0; - buffers.forEach((buffer) => length += buffer.byteLength == null ? buffer.length : buffer.byteLength); - const u82 = new Uint8Array(shared ? new SharedArrayBuffer(length) : length); - buffers.forEach((buffer) => { - const ref = Array.isArray(buffer) ? buffer : view(buffer, true); - u82.set(ref, offset); - offset += ref.length; - }); - return u82; -} - -// ../../Users/evan/GitHub/ImageScript/png/src/zlib.js -var u8 = Uint8Array; -var u16 = Uint16Array; -var u32 = Uint32Array; -var clim = u8.of(16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15); -var fleb = u8.of(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0); -var fdeb = u8.of(0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0); -var freb = function(eb, start) { - var b = new u16(31); - for (var i = 0; i < 31; ++i) { - b[i] = start += 1 << eb[i - 1]; - } - var r = new u32(b[30]); - for (var i = 1; i < 30; ++i) { - for (var j = b[i]; j < b[i + 1]; ++j) { - r[j] = j - b[i] << 5 | i; - } - } - return [b, r]; -}; -var _a = freb(fleb, 2); -var fl = _a[0]; -var revfl = _a[1]; -fl[28] = 258, revfl[258] = 28; -var _b = freb(fdeb, 0); -var fd = _b[0]; -var revfd = _b[1]; -var rev = new u16(32768); -for (var i = 0; i < 32768; ++i) { - x = (i & 43690) >>> 1 | (i & 21845) << 1; - x = (x & 52428) >>> 2 | (x & 13107) << 2; - x = (x & 61680) >>> 4 | (x & 3855) << 4; - rev[i] = ((x & 65280) >>> 8 | (x & 255) << 8) >>> 1; -} -var x; -var hMap = function(cd, mb, r) { - var s = cd.length; - var i = 0; - var l = new u16(mb); - for (; i < s; ++i) - ++l[cd[i] - 1]; - var le = new u16(mb); - for (i = 0; i < mb; ++i) { - le[i] = le[i - 1] + l[i - 1] << 1; - } - var co; - if (r) { - co = new u16(1 << mb); - var rvb = 15 - mb; - for (i = 0; i < s; ++i) { - if (cd[i]) { - var sv = i << 4 | cd[i]; - var r_1 = mb - cd[i]; - var v = le[cd[i] - 1]++ << r_1; - for (var m = v | (1 << r_1) - 1; v <= m; ++v) { - co[rev[v] >>> rvb] = sv; - } - } - } - } else { - co = new u16(s); - for (i = 0; i < s; ++i) { - if (cd[i]) { - co[i] = rev[le[cd[i] - 1]++] >>> 15 - cd[i]; - } - } - } - return co; -}; -var flt = new u8(288); -for (var i = 0; i < 144; ++i) - flt[i] = 8; -for (var i = 144; i < 256; ++i) - flt[i] = 9; -for (var i = 256; i < 280; ++i) - flt[i] = 7; -for (var i = 280; i < 288; ++i) - flt[i] = 8; -var fdt = new u8(32); -for (var i = 0; i < 32; ++i) - fdt[i] = 5; -var flm = hMap(flt, 9, 0); -var flrm = hMap(flt, 9, 1); -var fdm = hMap(fdt, 5, 0); -var fdrm = hMap(fdt, 5, 1); -var max = function(a) { - var m = a[0]; - for (var i = 1; i < a.length; ++i) { - if (a[i] > m) - m = a[i]; - } - return m; -}; -var bits = function(d, p, m) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8) >> (p & 7) & m; -}; -var bits16 = function(d, p) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); -}; -var shft = function(p) { - return (p >> 3 | 0) + (p & 7 && 1); -}; -var slc = function(v, s, e) { - if (s == null || s < 0) - s = 0; - if (e == null || e > v.length) - e = v.length; - var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s); - n.set(v.subarray(s, e)); - return n; -}; -var inflt = function(dat, buf, st) { - var sl = dat.length; - if (!sl || st && !st.l && sl < 5) - return buf || new u8(0); - var noBuf = !buf || st; - var noSt = !st || st.i; - if (!st) - st = {}; - if (!buf) - buf = new u8(sl * 3); - var cbuf = function(l2) { - var bl = buf.length; - if (l2 > bl) { - var nbuf = new u8(Math.max(bl * 2, l2)); - nbuf.set(buf); - buf = nbuf; - } - }; - var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; - var tbts = sl * 8; - do { - if (!lm) { - st.f = final = bits(dat, pos, 1); - var type = bits(dat, pos + 1, 3); - pos += 3; - if (!type) { - var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; - if (t > sl) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + l); - buf.set(dat.subarray(s, t), bt); - st.b = bt += l, st.p = pos = t * 8; - continue; - } else if (type === 1) - lm = flrm, dm = fdrm, lbt = 9, dbt = 5; - else if (type === 2) { - var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; - var tl = hLit + bits(dat, pos + 5, 31) + 1; - pos += 14; - var ldt = new u8(tl); - var clt = new u8(19); - for (var i = 0; i < hcLen; ++i) { - clt[clim[i]] = bits(dat, pos + i * 3, 7); - } - pos += hcLen * 3; - var clb = max(clt), clbmsk = (1 << clb) - 1; - var clm = hMap(clt, clb, 1); - for (var i = 0; i < tl; ) { - var r = clm[bits(dat, pos, clbmsk)]; - pos += r & 15; - var s = r >>> 4; - if (s < 16) { - ldt[i++] = s; - } else { - var c = 0, n = 0; - if (s === 16) - n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; - else if (s === 17) - n = 3 + bits(dat, pos, 7), pos += 3; - else if (s === 18) - n = 11 + bits(dat, pos, 127), pos += 7; - while (n--) - ldt[i++] = c; - } - } - var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); - lbt = max(lt); - dbt = max(dt); - lm = hMap(lt, lbt, 1); - dm = hMap(dt, dbt, 1); - } else - throw "invalid block type"; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - } - if (noBuf) - cbuf(bt + 131072); - var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; - var lpos = pos; - for (; ; lpos = pos) { - var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; - pos += c & 15; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (!c) - throw "invalid length/literal"; - if (sym < 256) - buf[bt++] = sym; - else if (sym === 256) { - lpos = pos, lm = null; - break; - } else { - var add = sym - 254; - if (sym > 264) { - var i = sym - 257, b = fleb[i]; - add = bits(dat, pos, (1 << b) - 1) + fl[i]; - pos += b; - } - var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; - if (!d) - throw "invalid distance"; - pos += d & 15; - var dt = fd[dsym]; - if (dsym > 3) { - var b = fdeb[dsym]; - dt += bits16(dat, pos) & (1 << b) - 1, pos += b; - } - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + 131072); - var end = bt + add; - for (; bt < end; bt += 4) { - buf[bt] = buf[bt - dt]; - buf[bt + 1] = buf[bt + 1 - dt]; - buf[bt + 2] = buf[bt + 2 - dt]; - buf[bt + 3] = buf[bt + 3 - dt]; - } - bt = end; - } - } - st.l = lm, st.p = lpos, st.b = bt; - if (lm) - final = 1, st.m = lbt, st.d = dm, st.n = dbt; - } while (!final); - return bt === buf.length ? buf : slc(buf, 0, bt); -}; -var wbits = function(d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; -}; -var wbits16 = function(d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; - d[o + 2] |= v >>> 16; -}; -var hTree = function(d, mb) { - var t = []; - for (var i = 0; i < d.length; ++i) { - if (d[i]) - t.push({s: i, f: d[i]}); - } - var s = t.length; - var t2 = t.slice(); - if (!s) - return [et, 0]; - if (s === 1) { - var v = new u8(t[0].s + 1); - v[t[0].s] = 1; - return [v, 1]; - } - t.sort(function(a, b) { - return a.f - b.f; - }); - t.push({s: -1, f: 25001}); - var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; - t[0] = {s: -1, f: l.f + r.f, l, r}; - while (i1 !== s - 1) { - l = t[t[i0].f < t[i2].f ? i0++ : i2++]; - r = t[i0 !== i1 && t[i0].f < t[i2].f ? i0++ : i2++]; - t[i1++] = {s: -1, f: l.f + r.f, l, r}; - } - var maxSym = t2[0].s; - for (var i = 1; i < s; ++i) { - if (t2[i].s > maxSym) - maxSym = t2[i].s; - } - var tr = new u16(maxSym + 1); - var mbt = ln(t[i1 - 1], tr, 0); - if (mbt > mb) { - var i = 0, dt = 0; - var lft = mbt - mb, cst = 1 << lft; - t2.sort(function(a, b) { - return tr[b.s] - tr[a.s] || a.f - b.f; - }); - for (; i < s; ++i) { - var i2_1 = t2[i].s; - if (tr[i2_1] > mb) { - dt += cst - (1 << mbt - tr[i2_1]); - tr[i2_1] = mb; - } else - break; - } - dt >>>= lft; - while (dt > 0) { - var i2_2 = t2[i].s; - if (tr[i2_2] < mb) - dt -= 1 << mb - tr[i2_2]++ - 1; - else - ++i; - } - for (; i >= 0 && dt; --i) { - var i2_3 = t2[i].s; - if (tr[i2_3] === mb) { - --tr[i2_3]; - ++dt; - } - } - mbt = mb; - } - return [new u8(tr), mbt]; -}; -var ln = function(n, l, d) { - return n.s === -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d; -}; -var lc = function(c) { - var s = c.length; - while (s && !c[--s]) - ; - var cl = new u16(++s); - var cli = 0, cln = c[0], cls = 1; - var w = function(v) { - cl[cli++] = v; - }; - for (var i = 1; i <= s; ++i) { - if (c[i] === cln && i !== s) - ++cls; - else { - if (!cln && cls > 2) { - for (; cls > 138; cls -= 138) - w(32754); - if (cls > 2) { - w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); - cls = 0; - } - } else if (cls > 3) { - w(cln), --cls; - for (; cls > 6; cls -= 6) - w(8304); - if (cls > 2) - w(cls - 3 << 5 | 8208), cls = 0; - } - while (cls--) - w(cln); - cls = 1; - cln = c[i]; - } - } - return [cl.subarray(0, cli), s]; -}; -var clen = function(cf, cl) { - var l = 0; - for (var i = 0; i < cl.length; ++i) - l += cf[i] * cl[i]; - return l; -}; -var wfblk = function(out, pos, dat) { - var s = dat.length; - var o = shft(pos + 2); - out[o] = s & 255; - out[o + 1] = s >>> 8; - out[o + 2] = out[o] ^ 255; - out[o + 3] = out[o + 1] ^ 255; - for (var i = 0; i < s; ++i) - out[o + i + 4] = dat[i]; - return (o + 4 + s) * 8; -}; -var wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) { - wbits(out, p++, final); - ++lf[256]; - var _a2 = hTree(lf, 15), dlt = _a2[0], mlb = _a2[1]; - var _b2 = hTree(df, 15), ddt = _b2[0], mdb = _b2[1]; - var _c = lc(dlt), lclt = _c[0], nlc = _c[1]; - var _d = lc(ddt), lcdt = _d[0], ndc = _d[1]; - var lcfreq = new u16(19); - for (var i = 0; i < lclt.length; ++i) - lcfreq[lclt[i] & 31]++; - for (var i = 0; i < lcdt.length; ++i) - lcfreq[lcdt[i] & 31]++; - var _e = hTree(lcfreq, 7), lct = _e[0], mlcb = _e[1]; - var nlcc = 19; - for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) - ; - var flen = bl + 5 << 3; - var ftlen = clen(lf, flt) + clen(df, fdt) + eb; - var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + (2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]); - if (flen <= ftlen && flen <= dtlen) - return wfblk(out, p, dat.subarray(bs, bs + bl)); - var lm, ll, dm, dl; - wbits(out, p, 1 + (dtlen < ftlen)), p += 2; - if (dtlen < ftlen) { - lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; - var llm = hMap(lct, mlcb, 0); - wbits(out, p, nlc - 257); - wbits(out, p + 5, ndc - 1); - wbits(out, p + 10, nlcc - 4); - p += 14; - for (var i = 0; i < nlcc; ++i) - wbits(out, p + 3 * i, lct[clim[i]]); - p += 3 * nlcc; - var lcts = [lclt, lcdt]; - for (var it = 0; it < 2; ++it) { - var clct = lcts[it]; - for (var i = 0; i < clct.length; ++i) { - var len = clct[i] & 31; - wbits(out, p, llm[len]), p += lct[len]; - if (len > 15) - wbits(out, p, clct[i] >>> 5 & 127), p += clct[i] >>> 12; - } - } - } else { - lm = flm, ll = flt, dm = fdm, dl = fdt; - } - for (var i = 0; i < li; ++i) { - if (syms[i] > 255) { - var len = syms[i] >>> 18 & 31; - wbits16(out, p, lm[len + 257]), p += ll[len + 257]; - if (len > 7) - wbits(out, p, syms[i] >>> 23 & 31), p += fleb[len]; - var dst = syms[i] & 31; - wbits16(out, p, dm[dst]), p += dl[dst]; - if (dst > 3) - wbits16(out, p, syms[i] >>> 5 & 8191), p += fdeb[dst]; - } else { - wbits16(out, p, lm[syms[i]]), p += ll[syms[i]]; - } - } - wbits16(out, p, lm[256]); - return p + ll[256]; -}; -var deo = u32.of(65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632); -var et = new u8(0); -var dflt = function(dat, lvl, plvl, pre, post, lst) { - var s = dat.length; - var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7e3)) + post); - var w = o.subarray(pre, o.length - post); - var pos = 0; - if (!lvl || s < 8) { - for (var i = 0; i <= s; i += 65535) { - var e = i + 65535; - if (e < s) { - pos = wfblk(w, pos, dat.subarray(i, e)); - } else { - w[i] = lst; - pos = wfblk(w, pos, dat.subarray(i, s)); - } - } - } else { - var opt = deo[lvl - 1]; - var n = opt >>> 13, c = opt & 8191; - var msk_1 = (1 << plvl) - 1; - var prev = new u16(32768), head = new u16(msk_1 + 1); - var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; - var hsh = function(i2) { - return (dat[i2] ^ dat[i2 + 1] << bs1_1 ^ dat[i2 + 2] << bs2_1) & msk_1; - }; - var syms = new u32(25e3); - var lf = new u16(288), df = new u16(32); - var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; - for (; i < s; ++i) { - var hv = hsh(i); - var imod = i & 32767, pimod = head[hv]; - prev[imod] = pimod; - head[hv] = imod; - if (wi <= i) { - var rem = s - i; - if ((lc_1 > 7e3 || li > 24576) && rem > 423) { - pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); - li = lc_1 = eb = 0, bs = i; - for (var j = 0; j < 286; ++j) - lf[j] = 0; - for (var j = 0; j < 30; ++j) - df[j] = 0; - } - var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; - if (rem > 2 && hv === hsh(i - dif)) { - var maxn = Math.min(n, rem) - 1; - var maxd = Math.min(32767, i); - var ml = Math.min(258, rem); - while (dif <= maxd && --ch_1 && imod !== pimod) { - if (dat[i + l] === dat[i + l - dif]) { - var nl = 0; - for (; nl < ml && dat[i + nl] === dat[i + nl - dif]; ++nl) - ; - if (nl > l) { - l = nl, d = dif; - if (nl > maxn) - break; - var mmd = Math.min(dif, nl - 2); - var md = 0; - for (var j = 0; j < mmd; ++j) { - var ti = i - dif + j + 32768 & 32767; - var pti = prev[ti]; - var cd = ti - pti + 32768 & 32767; - if (cd > md) - md = cd, pimod = ti; - } - } - } - imod = pimod, pimod = prev[imod]; - dif += imod - pimod + 32768 & 32767; - } - } - if (d) { - syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; - var lin = revfl[l] & 31, din = revfd[d] & 31; - eb += fleb[lin] + fdeb[din]; - ++lf[257 + lin]; - ++df[din]; - wi = i + l; - ++lc_1; - } else { - syms[li++] = dat[i]; - ++lf[dat[i]]; - } - } - } - pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); - if (!lst && pos & 7) - pos = wfblk(w, pos + 1, et); - } - return slc(o, 0, pre + shft(pos) + post); -}; -var adler = function() { - var a = 1, b = 0; - return { - p: function(d) { - var n = a, m = b; - var l = d.length | 0; - for (var i = 0; i !== l; ) { - var e = Math.min(i + 2655, l); - for (; i < e; ++i) - m += n += d[i]; - n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16); - } - a = n, b = m; - }, - d: function() { - a %= 65521, b %= 65521; - return (a & 255) << 24 | a >>> 8 << 16 | (b & 255) << 8 | b >>> 8; - } - }; -}; -var dopt = function(dat, opt, pre, post, st) { - return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 12 + opt.mem, pre, post, !st); -}; -var wbytes = function(d, b, v) { - for (; v; ++b) - d[b] = v, v >>>= 8; -}; -var zlh = function(c, o) { - var lv = o.level, fl2 = lv === 0 ? 0 : lv < 6 ? 1 : lv === 9 ? 3 : 2; - c[0] = 120, c[1] = fl2 << 6 | (fl2 ? 32 - 2 * fl2 : 1); -}; -var zlv = function(d) { - if ((d[0] & 15) !== 8 || d[0] >>> 4 > 7 || (d[0] << 8 | d[1]) % 31) - throw "invalid zlib data"; - if (d[1] & 32) - throw "invalid zlib data: preset dictionaries not supported"; -}; -function zlibSync(data, opts) { - if (!opts) - opts = {}; - var a = adler(); - a.p(data); - var d = dopt(data, opts, 2, 4); - return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; -} -function unzlibSync(data, out) { - return inflt((zlv(data), data.subarray(2, -4)), out); -} -function compress(buf, level) { - return zlibSync(buf, {level}); -} -function decompress(buf, limit) { - try { - return unzlibSync(buf, new Uint8Array(limit)); - } catch (err) { - throw err.message ? err : new Error(err); - } -} - -// ../../Users/evan/GitHub/ImageScript/png/src/png.js -var __IHDR__ = new Uint8Array([73, 72, 68, 82]); -var __IDAT__ = new Uint8Array([73, 68, 65, 84]); -var __IEND__ = new Uint8Array([73, 69, 78, 68]); -var __IEND_CRC__ = crc32(new Uint8Array([73, 69, 78, 68])); -var HEAD = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); -var color_types = { - GREYSCALE: 0, - TRUECOLOR: 2, - INDEXED_COLOR: 3, - GREYSCALE_ALPHA: 4, - TRUECOLOR_ALPHA: 6 -}; -var channels_to_color_type = { - 1: color_types.GREYSCALE, - 2: color_types.GREYSCALE_ALPHA, - 3: color_types.TRUECOLOR, - 4: color_types.TRUECOLOR_ALPHA -}; -var utf8encoder = new TextEncoder(); -function encode(data, {text, width, height, channels, depth = 8, level = 0}) { - let offset = 0; - let tmp_offset = 0; - const row_length = width * channels; - const tmp = new Uint8Array(height + data.length); - while (offset < data.length) { - tmp[tmp_offset++] = 0; - tmp.set(data.subarray(offset, offset += row_length), tmp_offset); - tmp_offset += row_length; - } - if (text) { - let chunks = []; - for (const key in text) { - if (!text[key]) - continue; - const kb = utf8encoder.encode(key); - const tb = utf8encoder.encode(text[key]); - const chunk = new Uint8Array(1 + 12 + kb.length + tb.length); - const view3 = new DataView(chunk.buffer); - chunk[4] = 116; - chunk[5] = 69; - chunk[6] = 88; - chunk[7] = 116; - chunk.set(kb, 8); - chunks.push(chunk); - chunk.set(tb, 9 + kb.length); - view3.setUint32(0, chunk.length - 12); - view3.setUint32(chunk.length - 4, crc32(chunk.subarray(4, chunk.length - 4))); - } - text = from_parts(chunks); - } - offset = text ? text.length : 0; - const compressed = compress(tmp, level); - const array = new Uint8Array(49 + offset + HEAD.length + compressed.length); - array[26] = 0; - array[27] = 0; - array[28] = 0; - array[24] = depth; - array.set(HEAD, 0); - array.set(__IHDR__, 12); - array.set(__IDAT__, 37); - array.set(compressed, 41); - array[25] = channels_to_color_type[channels]; - if (text) - array.set(text, 45 + compressed.length); - array.set(__IEND__, 49 + offset + compressed.length); - const view2 = new DataView(array.buffer); - view2.setUint32(8, 13); - view2.setUint32(16, width); - view2.setUint32(20, height); - view2.setUint32(33, compressed.length); - view2.setUint32(45 + offset + compressed.length, 0); - view2.setUint32(53 + offset + compressed.length, __IEND_CRC__); - view2.setUint32(29, crc32(new Uint8Array(array.buffer, 12, 17))); - view2.setUint32(41 + compressed.length, crc32(new Uint8Array(array.buffer, 37, 4 + compressed.length))); - return array; -} -function decode(array) { - let view2 = new DataView(array.buffer, array.byteOffset, array.byteLength); - const width = view2.getUint32(16); - const height = view2.getUint32(20); - const bpc = array[24]; - const pixel_type = array[25]; - let channels = {3: 1, 0: 1, 4: 2, 2: 3, 6: 4}[pixel_type]; - const bytespp = channels * bpc / 8; - const row_length = width * bytespp; - let pixels = new Uint8Array(height * row_length); - let offset = 0; - let p_offset = 0; - let c_offset = 33; - const chunks = []; - let palette, alphaPalette; - const maxSearchOffset = array.length - 5; - let type; - while ((type = view2.getUint32(4 + c_offset)) !== 1229278788) { - if (type === 1229209940) - chunks.push(array.subarray(8 + c_offset, 8 + c_offset + view2.getUint32(c_offset))); - else if (type === 1347179589) { - if (palette) - throw new Error("PLTE can only occur once in an image"); - palette = new Uint32Array(view2.getUint32(c_offset)); - for (let pxlOffset = 0; pxlOffset < palette.length * 8; pxlOffset += 3) - palette[pxlOffset / 3] = array[8 + c_offset + pxlOffset] << 24 | array[8 + c_offset + pxlOffset + 1] << 16 | array[8 + c_offset + pxlOffset + 2] << 8 | 255; - } else if (type === 1951551059) { - if (alphaPalette) - throw new Error("tRNS can only occur once in an image"); - alphaPalette = new Uint8Array(view2.getUint32(c_offset)); - for (let i = 0; i < alphaPalette.length; i++) - alphaPalette[i] = array[8 + c_offset + i]; - } - c_offset += 4 + 4 + 4 + view2.getUint32(c_offset); - if (c_offset > maxSearchOffset) - break; - } - array = decompress(chunks.length === 1 ? chunks[0] : from_parts(chunks), height + height * row_length); - while (offset < array.byteLength) { - const filter = array[offset++]; - const slice = array.subarray(offset, offset += row_length); - if (filter === 0) - pixels.set(slice, p_offset); - else if (filter === 1) - filter_1(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 2) - filter_2(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 3) - filter_3(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 4) - filter_4(slice, pixels, p_offset, bytespp, row_length); - p_offset += row_length; - } - if (pixel_type === 3) { - if (!palette) - throw new Error("Indexed color PNG has no PLTE"); - if (alphaPalette) - for (let i = 0; i < alphaPalette.length; i++) - palette[i] &= 4294967040 | alphaPalette[i]; - channels = 4; - const newPixels = new Uint8Array(width * height * 4); - const pixelView = new DataView(newPixels.buffer, newPixels.byteOffset, newPixels.byteLength); - for (let i = 0; i < pixels.length; i++) - pixelView.setUint32(i * 4, palette[pixels[i]], false); - pixels = newPixels; - } - if (bpc !== 8) { - const newPixels = new Uint8Array(pixels.length / bpc * 8); - for (let i = 0; i < pixels.length; i += 2) - newPixels[i / 2] = pixels[i]; - pixels = newPixels; - } - if (channels !== 4) { - const newPixels = new Uint8Array(width * height * 4); - const view3 = new DataView(newPixels.buffer); - if (channels === 1) { - for (let i = 0; i < width * height; i++) { - const pixel = pixels[i]; - view3.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 255, false); - } - } else if (channels === 2) { - for (let i = 0; i < width * height * 2; i += 2) { - const pixel = pixels[i]; - view3.setUint32(i * 2, pixel << 24 | pixel << 16 | pixel << 8 | pixels[i + 1], false); - } - } else if (channels === 3) { - newPixels.fill(255); - for (let i = 0; i < width * height; i++) - newPixels.set(pixels.subarray(i * 3, i * 3 + 3), i * 4); - } - pixels = newPixels; - } - return {width, height, pixels}; -} -function filter_1(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - while (i < bytespp) - pixels[i + p_offset] = slice[i++]; - while (i < row_length) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - bytespp]; -} -function filter_2(slice, pixels, p_offset, bytespp, row_length) { - if (p_offset === 0) - pixels.set(slice, p_offset); - else { - let i = 0; - while (i < row_length) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - } -} -function filter_3(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - if (p_offset === 0) { - while (i < bytespp) - pixels[i] = slice[i++]; - while (i < row_length) - pixels[i] = slice[i] + (pixels[i++ - bytespp] >> 1); - } else { - while (i < bytespp) - pixels[i + p_offset] = slice[i] + (pixels[i++ + p_offset - row_length] >> 1); - while (i < row_length) - pixels[i + p_offset] = slice[i] + (pixels[i + p_offset - bytespp] + pixels[i++ + p_offset - row_length] >> 1); - } -} -function filter_4(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - if (p_offset === 0) { - while (i < bytespp) - pixels[i] = slice[i++]; - while (i < row_length) - pixels[i] = slice[i] + pixels[i++ - bytespp]; - } else { - while (i < bytespp) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - while (i < row_length) { - const a = pixels[i + p_offset - bytespp]; - const b = pixels[i + p_offset - row_length]; - const c = pixels[i + p_offset - bytespp - row_length]; - const p = a + b - c; - const pa = Math.abs(p - a); - const pb = Math.abs(p - b); - const pc = Math.abs(p - c); - pixels[i + p_offset] = slice[i++] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c); - } - } -} diff --git a/png/src/crc.mjs b/png/src/crc.mjs deleted file mode 100644 index 131b39e..0000000 --- a/png/src/crc.mjs +++ /dev/null @@ -1,49 +0,0 @@ -const table = new Uint32Array([ - 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, - 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, - 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, - 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, - 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, - 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, - 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, - 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, - 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, - 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, - 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, - 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, - 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, - 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, - 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, - 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, - 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, - 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, - 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, - 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, - 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, - 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, - 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, - 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, - 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, - 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, - 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, - 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, - 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]); - -export function crc32(buffer) { - let offset = 0 | 0; - let crc = 0xFFFFFFFF | 0; - const bl = (buffer.length - 4) | 0; - - while (bl > offset) { - crc = table[(crc ^ buffer[offset++]) & 0xff] ^ (crc >>> 8); - crc = table[(crc ^ buffer[offset++]) & 0xff] ^ (crc >>> 8); - crc = table[(crc ^ buffer[offset++]) & 0xff] ^ (crc >>> 8); - crc = table[(crc ^ buffer[offset++]) & 0xff] ^ (crc >>> 8); - } - - while (offset < buffer.length) { - crc = table[(crc ^ buffer[offset++]) & 0xff] ^ (crc >>> 8); - } - - return (crc ^ 0xFFFFFFFF) >>> 0; -} \ No newline at end of file diff --git a/png/src/mem.mjs b/png/src/mem.mjs deleted file mode 100644 index ce6114e..0000000 --- a/png/src/mem.mjs +++ /dev/null @@ -1,24 +0,0 @@ -export function view(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} - -export function from_parts(buffers, shared = false) { - let length = 0; - let offset = 0; - buffers.forEach(buffer => length += (null == buffer.byteLength ? buffer.length : buffer.byteLength)); - - const u8 = new Uint8Array(shared ? new SharedArrayBuffer(length) : length); - - buffers.forEach(buffer => { - const ref = Array.isArray(buffer) ? buffer : view(buffer, true); - - u8.set(ref, offset); - offset += ref.length; - }); - - return u8; -} \ No newline at end of file diff --git a/png/src/png.mjs b/png/src/png.mjs deleted file mode 100644 index 62848d4..0000000 --- a/png/src/png.mjs +++ /dev/null @@ -1,254 +0,0 @@ -import { crc32 } from './crc.mjs'; -import { from_parts } from './mem.mjs'; -import { compress, decompress } from './zlib.mjs'; - -const __IHDR__ = new Uint8Array([73, 72, 68, 82]); -const __IDAT__ = new Uint8Array([73, 68, 65, 84]); -const __IEND__ = new Uint8Array([73, 69, 78, 68]); -const __IEND_CRC__ = crc32(new Uint8Array([73, 69, 78, 68])); -const HEAD = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); - -const color_types = { - GREYSCALE: 0, - TRUECOLOR: 2, - INDEXED_COLOR: 3, - GREYSCALE_ALPHA: 4, - TRUECOLOR_ALPHA: 6 -}; - -const channels_to_color_type = { - 1: color_types.GREYSCALE, - 2: color_types.GREYSCALE_ALPHA, - - 3: color_types.TRUECOLOR, - 4: color_types.TRUECOLOR_ALPHA -}; - -const utf8encoder = new TextEncoder; // replace with latin1 encoder or iext - -export function encode(data, { text, width, height, channels, depth = 8, level = 0 }) { - let offset = 0; - let tmp_offset = 0; - const row_length = width * channels; - const tmp = new Uint8Array(height + data.length); - - while (offset < data.length) { - tmp[tmp_offset++] = 0; - tmp.set(data.subarray(offset, (offset += row_length)), tmp_offset); - - tmp_offset += row_length; - } - - if (text) { - let chunks = []; - for (const key in text) { - if (!text[key]) continue; - const kb = utf8encoder.encode(key); - const tb = utf8encoder.encode(text[key]); - const chunk = new Uint8Array(1 + 12 + kb.length + tb.length); - - const view = new DataView(chunk.buffer); - - chunk[4] = 0x74; - chunk[5] = 0x45; - chunk[6] = 0x58; - chunk[7] = 0x74; - chunk.set(kb, 8); - chunks.push(chunk); - chunk.set(tb, 9 + kb.length); - view.setUint32(0, chunk.length - 12); - view.setUint32(chunk.length - 4, crc32(chunk.subarray(4, chunk.length - 4))); - } - - text = from_parts(chunks); - } - - offset = text ? text.length : 0; - const compressed = compress(tmp, level); - const array = new Uint8Array(49 + offset + HEAD.length + compressed.length); - - array[26] = 0; - array[27] = 0; - array[28] = 0; - array[24] = depth; - array.set(HEAD, 0); - array.set(__IHDR__, 12); - array.set(__IDAT__, 37); - array.set(compressed, 41); - array[25] = channels_to_color_type[channels]; - if (text) array.set(text, 45 + compressed.length); - array.set(__IEND__, 49 + offset + compressed.length); - - const view = new DataView(array.buffer); - - view.setUint32(8, 13); - view.setUint32(16, width); - view.setUint32(20, height); - view.setUint32(33, compressed.length); - view.setUint32(45 + offset + compressed.length, 0); - view.setUint32(53 + offset + compressed.length, __IEND_CRC__); - view.setUint32(29, crc32(new Uint8Array(array.buffer, 12, 17))); - view.setUint32(41 + compressed.length, crc32(new Uint8Array(array.buffer, 37, 4 + compressed.length))); - - return array; -} - -export function decode(array) { - let view = new DataView(array.buffer, array.byteOffset, array.byteLength); - - const width = view.getUint32(16); - const height = view.getUint32(20); - const bpc = array[24]; - const pixel_type = array[25]; - let channels = ({ 3: 1, 0: 1, 4: 2, 2: 3, 6: 4 })[pixel_type]; - const bytespp = channels * bpc / 8; - - const row_length = width * bytespp; - let pixels = new Uint8Array(height * row_length); - - let offset = 0; - let p_offset = 0; - - let c_offset = 33; - const chunks = []; - - let palette, alphaPalette; - - const maxSearchOffset = array.length - 5; - - let type; - while ((type = view.getUint32(4 + c_offset)) !== 1229278788) { // IEND - if (type === 1229209940) // IDAT - chunks.push(array.subarray(8 + c_offset, 8 + c_offset + view.getUint32(c_offset))); - else if (type === 1347179589) { // PLTE - if (palette) - throw new Error('PLTE can only occur once in an image'); - palette = new Uint32Array(view.getUint32(c_offset)); - for (let pxlOffset = 0; pxlOffset < palette.length * 8; pxlOffset += 3) - palette[pxlOffset / 3] = array[8 + c_offset + pxlOffset] << 24 | array[8 + c_offset + pxlOffset + 1] << 16 | array[8 + c_offset + pxlOffset + 2] << 8 | 0xff; - } else if (type === 1951551059) { // tRNS - if (alphaPalette) - throw new Error('tRNS can only occur once in an image'); - alphaPalette = new Uint8Array(view.getUint32(c_offset)); - for (let i = 0; i < alphaPalette.length; i++) - alphaPalette[i] = array[8 + c_offset + i]; - } - - c_offset += 4 + 4 + 4 + view.getUint32(c_offset); - if (c_offset > maxSearchOffset) // missing IEND - break; - } - - array = decompress(chunks.length === 1 ? chunks[0] : from_parts(chunks), height + height * row_length); - - while (offset < array.byteLength) { - const filter = array[offset++]; - const slice = array.subarray(offset, offset += row_length); - - if (0 === filter) pixels.set(slice, p_offset); - else if (1 === filter) filter_1(slice, pixels, p_offset, bytespp, row_length); - else if (2 === filter) filter_2(slice, pixels, p_offset, bytespp, row_length); - else if (3 === filter) filter_3(slice, pixels, p_offset, bytespp, row_length); - else if (4 === filter) filter_4(slice, pixels, p_offset, bytespp, row_length); - - p_offset += row_length; - } - - if (pixel_type === 3) { - if (!palette) - throw new Error('Indexed color PNG has no PLTE'); - - if (alphaPalette) - for (let i = 0; i < alphaPalette.length; i++) - palette[i] &= 0xffffff00 | alphaPalette[i]; - - channels = 4; - const newPixels = new Uint8Array(width * height * 4); - const pixelView = new DataView(newPixels.buffer, newPixels.byteOffset, newPixels.byteLength); - for (let i = 0; i < pixels.length; i++) - pixelView.setUint32(i * 4, palette[pixels[i]], false); - pixels = newPixels; - } - - if (bpc !== 8) { - const newPixels = new Uint8Array(pixels.length / bpc * 8); - for (let i = 0; i < pixels.length; i += 2) - newPixels[i / 2] = pixels[i]; - pixels = newPixels; - } - - if (channels !== 4) { - const newPixels = new Uint8Array(width * height * 4); - const view = new DataView(newPixels.buffer); - - if (channels === 1) { - for (let i = 0; i < width * height; i++) { - const pixel = pixels[i]; - view.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 0xff, false); - } - } else if (channels === 2) { - for (let i = 0; i < width * height * 2; i += 2) { - const pixel = pixels[i]; - view.setUint32(i * 2, pixel << 24 | pixel << 16 | pixel << 8 | pixels[i + 1], false); - } - } else if (channels === 3) { - newPixels.fill(0xff); - for (let i = 0; i < width * height; i++) - newPixels.set(pixels.subarray(i * 3, i * 3 + 3), i * 4); - } - - pixels = newPixels; - } - - return { width, height, buffer: pixels }; -} - -function filter_1(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - while (i < bytespp) pixels[i + p_offset] = slice[i++]; - while (i < row_length) pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - bytespp]; -} - -function filter_2(slice, pixels, p_offset, bytespp, row_length) { - if (0 === p_offset) pixels.set(slice, p_offset); - else { - let i = 0; - while (i < row_length) pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - } -} - -function filter_3(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - - if (0 === p_offset) { - while (i < bytespp) pixels[i] = slice[i++]; - while (i < row_length) pixels[i] = slice[i] + (pixels[i++ - bytespp] >> 1); - } else { - while (i < bytespp) pixels[i + p_offset] = slice[i] + (pixels[i++ + p_offset - row_length] >> 1); - while (i < row_length) pixels[i + p_offset] = slice[i] + (pixels[i + p_offset - bytespp] + pixels[i++ + p_offset - row_length] >> 1); - } -} - -function filter_4(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - - if (0 === p_offset) { - while (i < bytespp) pixels[i] = slice[i++]; - while (i < row_length) pixels[i] = slice[i] + pixels[i++ - bytespp]; - } else { - while (i < bytespp) pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - - while (i < row_length) { - const a = pixels[i + p_offset - bytespp]; - const b = pixels[i + p_offset - row_length]; - const c = pixels[i + p_offset - bytespp - row_length]; - - const p = a + b - c; - const pa = Math.abs(p - a); - const pb = Math.abs(p - b); - const pc = Math.abs(p - c); - - pixels[i + p_offset] = slice[i++] + ((pa <= pb && pa <= pc) ? a : ((pb <= pc) ? b : c)); - } - } -} diff --git a/png/src/zlib.mjs b/png/src/zlib.mjs deleted file mode 100644 index 93f331b..0000000 --- a/png/src/zlib.mjs +++ /dev/null @@ -1,618 +0,0 @@ -// node_modules/fflate/esm/browser.js -// https://github.com/101arrowz/fflate - -// MIT License - -// Copyright (c) 2020 Arjun Barrett - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - - -var u8 = Uint8Array; -var u16 = Uint16Array; -var u32 = Uint32Array; -var clim = u8.of(16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15); -var fleb = u8.of(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0); -var fdeb = u8.of(0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0); -var freb = function (eb, start) { - var b = new u16(31); - for (var i = 0; i < 31; ++i) { - b[i] = start += 1 << eb[i - 1]; - } - var r = new u32(b[30]); - for (var i = 1; i < 30; ++i) { - for (var j = b[i]; j < b[i + 1]; ++j) { - r[j] = j - b[i] << 5 | i; - } - } - return [b, r]; -}; -var _a = freb(fleb, 2); -var fl = _a[0]; -var revfl = _a[1]; -fl[28] = 258, revfl[258] = 28; -var _b = freb(fdeb, 0); -var fd = _b[0]; -var revfd = _b[1]; -var rev = new u16(32768); -for (var i = 0; i < 32768; ++i) { - x = (i & 43690) >>> 1 | (i & 21845) << 1; - x = (x & 52428) >>> 2 | (x & 13107) << 2; - x = (x & 61680) >>> 4 | (x & 3855) << 4; - rev[i] = ((x & 65280) >>> 8 | (x & 255) << 8) >>> 1; -} -var x; -var hMap = function (cd, mb, r) { - var s = cd.length; - var i = 0; - var l = new u16(mb); - for (; i < s; ++i) - ++l[cd[i] - 1]; - var le = new u16(mb); - for (i = 0; i < mb; ++i) { - le[i] = le[i - 1] + l[i - 1] << 1; - } - var co; - if (r) { - co = new u16(1 << mb); - var rvb = 15 - mb; - for (i = 0; i < s; ++i) { - if (cd[i]) { - var sv = i << 4 | cd[i]; - var r_1 = mb - cd[i]; - var v = le[cd[i] - 1]++ << r_1; - for (var m = v | (1 << r_1) - 1; v <= m; ++v) { - co[rev[v] >>> rvb] = sv; - } - } - } - } else { - co = new u16(s); - for (i = 0; i < s; ++i) { - if (cd[i]) { - co[i] = rev[le[cd[i] - 1]++] >>> 15 - cd[i]; - } - } - } - return co; -}; -var flt = new u8(288); -for (var i = 0; i < 144; ++i) - flt[i] = 8; -for (var i = 144; i < 256; ++i) - flt[i] = 9; -for (var i = 256; i < 280; ++i) - flt[i] = 7; -for (var i = 280; i < 288; ++i) - flt[i] = 8; -var fdt = new u8(32); -for (var i = 0; i < 32; ++i) - fdt[i] = 5; -var flm = hMap(flt, 9, 0); -var flrm = hMap(flt, 9, 1); -var fdm = hMap(fdt, 5, 0); -var fdrm = hMap(fdt, 5, 1); -var max = function (a) { - var m = a[0]; - for (var i = 1; i < a.length; ++i) { - if (a[i] > m) - m = a[i]; - } - return m; -}; -var bits = function (d, p, m) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8) >> (p & 7) & m; -}; -var bits16 = function (d, p) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); -}; -var shft = function (p) { - return (p >> 3 | 0) + (p & 7 && 1); -}; -var slc = function (v, s, e) { - if (s == null || s < 0) - s = 0; - if (e == null || e > v.length) - e = v.length; - var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s); - n.set(v.subarray(s, e)); - return n; -}; -var inflt = function (dat, buf, st) { - var sl = dat.length; - if (!sl || st && !st.l && sl < 5) - return buf || new u8(0); - var noBuf = !buf || st; - var noSt = !st || st.i; - if (!st) - st = {}; - if (!buf) - buf = new u8(sl * 3); - var cbuf = function (l2) { - var bl = buf.length; - if (l2 > bl) { - var nbuf = new u8(Math.max(bl * 2, l2)); - nbuf.set(buf); - buf = nbuf; - } - }; - var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; - var tbts = sl * 8; - do { - if (!lm) { - st.f = final = bits(dat, pos, 1); - var type = bits(dat, pos + 1, 3); - pos += 3; - if (!type) { - var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; - if (t > sl) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + l); - buf.set(dat.subarray(s, t), bt); - st.b = bt += l, st.p = pos = t * 8; - continue; - } else if (type === 1) - lm = flrm, dm = fdrm, lbt = 9, dbt = 5; - else if (type === 2) { - var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; - var tl = hLit + bits(dat, pos + 5, 31) + 1; - pos += 14; - var ldt = new u8(tl); - var clt = new u8(19); - for (var i = 0; i < hcLen; ++i) { - clt[clim[i]] = bits(dat, pos + i * 3, 7); - } - pos += hcLen * 3; - var clb = max(clt), clbmsk = (1 << clb) - 1; - var clm = hMap(clt, clb, 1); - for (var i = 0; i < tl;) { - var r = clm[bits(dat, pos, clbmsk)]; - pos += r & 15; - var s = r >>> 4; - if (s < 16) { - ldt[i++] = s; - } else { - var c = 0, n = 0; - if (s === 16) - n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; - else if (s === 17) - n = 3 + bits(dat, pos, 7), pos += 3; - else if (s === 18) - n = 11 + bits(dat, pos, 127), pos += 7; - while (n--) - ldt[i++] = c; - } - } - var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); - lbt = max(lt); - dbt = max(dt); - lm = hMap(lt, lbt, 1); - dm = hMap(dt, dbt, 1); - } else - throw "invalid block type"; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - } - if (noBuf) - cbuf(bt + 131072); - var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; - var lpos = pos; - for (; ; lpos = pos) { - var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; - pos += c & 15; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (!c) - throw "invalid length/literal"; - if (sym < 256) - buf[bt++] = sym; - else if (sym === 256) { - lpos = pos, lm = null; - break; - } else { - var add = sym - 254; - if (sym > 264) { - var i = sym - 257, b = fleb[i]; - add = bits(dat, pos, (1 << b) - 1) + fl[i]; - pos += b; - } - var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; - if (!d) - throw "invalid distance"; - pos += d & 15; - var dt = fd[dsym]; - if (dsym > 3) { - var b = fdeb[dsym]; - dt += bits16(dat, pos) & (1 << b) - 1, pos += b; - } - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + 131072); - var end = bt + add; - for (; bt < end; bt += 4) { - buf[bt] = buf[bt - dt]; - buf[bt + 1] = buf[bt + 1 - dt]; - buf[bt + 2] = buf[bt + 2 - dt]; - buf[bt + 3] = buf[bt + 3 - dt]; - } - bt = end; - } - } - st.l = lm, st.p = lpos, st.b = bt; - if (lm) - final = 1, st.m = lbt, st.d = dm, st.n = dbt; - } while (!final); - return bt === buf.length ? buf : slc(buf, 0, bt); -}; -var wbits = function (d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; -}; -var wbits16 = function (d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; - d[o + 2] |= v >>> 16; -}; -var hTree = function (d, mb) { - var t = []; - for (var i = 0; i < d.length; ++i) { - if (d[i]) - t.push({ s: i, f: d[i] }); - } - var s = t.length; - var t2 = t.slice(); - if (!s) - return [et, 0]; - if (s === 1) { - var v = new u8(t[0].s + 1); - v[t[0].s] = 1; - return [v, 1]; - } - t.sort(function (a, b) { - return a.f - b.f; - }); - t.push({ s: -1, f: 25001 }); - var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; - t[0] = { s: -1, f: l.f + r.f, l, r }; - while (i1 !== s - 1) { - l = t[t[i0].f < t[i2].f ? i0++ : i2++]; - r = t[i0 !== i1 && t[i0].f < t[i2].f ? i0++ : i2++]; - t[i1++] = { s: -1, f: l.f + r.f, l, r }; - } - var maxSym = t2[0].s; - for (var i = 1; i < s; ++i) { - if (t2[i].s > maxSym) - maxSym = t2[i].s; - } - var tr = new u16(maxSym + 1); - var mbt = ln(t[i1 - 1], tr, 0); - if (mbt > mb) { - var i = 0, dt = 0; - var lft = mbt - mb, cst = 1 << lft; - t2.sort(function (a, b) { - return tr[b.s] - tr[a.s] || a.f - b.f; - }); - for (; i < s; ++i) { - var i2_1 = t2[i].s; - if (tr[i2_1] > mb) { - dt += cst - (1 << mbt - tr[i2_1]); - tr[i2_1] = mb; - } else - break; - } - dt >>>= lft; - while (dt > 0) { - var i2_2 = t2[i].s; - if (tr[i2_2] < mb) - dt -= 1 << mb - tr[i2_2]++ - 1; - else - ++i; - } - for (; i >= 0 && dt; --i) { - var i2_3 = t2[i].s; - if (tr[i2_3] === mb) { - --tr[i2_3]; - ++dt; - } - } - mbt = mb; - } - return [new u8(tr), mbt]; -}; -var ln = function (n, l, d) { - return n.s === -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d; -}; -var lc = function (c) { - var s = c.length; - while (s && !c[--s]) - ; - var cl = new u16(++s); - var cli = 0, cln = c[0], cls = 1; - var w = function (v) { - cl[cli++] = v; - }; - for (var i = 1; i <= s; ++i) { - if (c[i] === cln && i !== s) - ++cls; - else { - if (!cln && cls > 2) { - for (; cls > 138; cls -= 138) - w(32754); - if (cls > 2) { - w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); - cls = 0; - } - } else if (cls > 3) { - w(cln), --cls; - for (; cls > 6; cls -= 6) - w(8304); - if (cls > 2) - w(cls - 3 << 5 | 8208), cls = 0; - } - while (cls--) - w(cln); - cls = 1; - cln = c[i]; - } - } - return [cl.subarray(0, cli), s]; -}; -var clen = function (cf, cl) { - var l = 0; - for (var i = 0; i < cl.length; ++i) - l += cf[i] * cl[i]; - return l; -}; -var wfblk = function (out, pos, dat) { - var s = dat.length; - var o = shft(pos + 2); - out[o] = s & 255; - out[o + 1] = s >>> 8; - out[o + 2] = out[o] ^ 255; - out[o + 3] = out[o + 1] ^ 255; - for (var i = 0; i < s; ++i) - out[o + i + 4] = dat[i]; - return (o + 4 + s) * 8; -}; -var wblk = function (dat, out, final, syms, lf, df, eb, li, bs, bl, p) { - wbits(out, p++, final); - ++lf[256]; - var _a2 = hTree(lf, 15), dlt = _a2[0], mlb = _a2[1]; - var _b2 = hTree(df, 15), ddt = _b2[0], mdb = _b2[1]; - var _c = lc(dlt), lclt = _c[0], nlc = _c[1]; - var _d = lc(ddt), lcdt = _d[0], ndc = _d[1]; - var lcfreq = new u16(19); - for (var i = 0; i < lclt.length; ++i) - lcfreq[lclt[i] & 31]++; - for (var i = 0; i < lcdt.length; ++i) - lcfreq[lcdt[i] & 31]++; - var _e = hTree(lcfreq, 7), lct = _e[0], mlcb = _e[1]; - var nlcc = 19; - for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) - ; - var flen = bl + 5 << 3; - var ftlen = clen(lf, flt) + clen(df, fdt) + eb; - var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + (2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]); - if (flen <= ftlen && flen <= dtlen) - return wfblk(out, p, dat.subarray(bs, bs + bl)); - var lm, ll, dm, dl; - wbits(out, p, 1 + (dtlen < ftlen)), p += 2; - if (dtlen < ftlen) { - lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; - var llm = hMap(lct, mlcb, 0); - wbits(out, p, nlc - 257); - wbits(out, p + 5, ndc - 1); - wbits(out, p + 10, nlcc - 4); - p += 14; - for (var i = 0; i < nlcc; ++i) - wbits(out, p + 3 * i, lct[clim[i]]); - p += 3 * nlcc; - var lcts = [lclt, lcdt]; - for (var it = 0; it < 2; ++it) { - var clct = lcts[it]; - for (var i = 0; i < clct.length; ++i) { - var len = clct[i] & 31; - wbits(out, p, llm[len]), p += lct[len]; - if (len > 15) - wbits(out, p, clct[i] >>> 5 & 127), p += clct[i] >>> 12; - } - } - } else { - lm = flm, ll = flt, dm = fdm, dl = fdt; - } - for (var i = 0; i < li; ++i) { - if (syms[i] > 255) { - var len = syms[i] >>> 18 & 31; - wbits16(out, p, lm[len + 257]), p += ll[len + 257]; - if (len > 7) - wbits(out, p, syms[i] >>> 23 & 31), p += fleb[len]; - var dst = syms[i] & 31; - wbits16(out, p, dm[dst]), p += dl[dst]; - if (dst > 3) - wbits16(out, p, syms[i] >>> 5 & 8191), p += fdeb[dst]; - } else { - wbits16(out, p, lm[syms[i]]), p += ll[syms[i]]; - } - } - wbits16(out, p, lm[256]); - return p + ll[256]; -}; -var deo = u32.of(65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632); -var et = new u8(0); -var dflt = function (dat, lvl, plvl, pre, post, lst) { - var s = dat.length; - var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7e3)) + post); - var w = o.subarray(pre, o.length - post); - var pos = 0; - if (!lvl || s < 8) { - for (var i = 0; i <= s; i += 65535) { - var e = i + 65535; - if (e < s) { - pos = wfblk(w, pos, dat.subarray(i, e)); - } else { - w[i] = lst; - pos = wfblk(w, pos, dat.subarray(i, s)); - } - } - } else { - var opt = deo[lvl - 1]; - var n = opt >>> 13, c = opt & 8191; - var msk_1 = (1 << plvl) - 1; - var prev = new u16(32768), head = new u16(msk_1 + 1); - var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; - var hsh = function (i2) { - return (dat[i2] ^ dat[i2 + 1] << bs1_1 ^ dat[i2 + 2] << bs2_1) & msk_1; - }; - var syms = new u32(25e3); - var lf = new u16(288), df = new u16(32); - var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; - for (; i < s; ++i) { - var hv = hsh(i); - var imod = i & 32767, pimod = head[hv]; - prev[imod] = pimod; - head[hv] = imod; - if (wi <= i) { - var rem = s - i; - if ((lc_1 > 7e3 || li > 24576) && rem > 423) { - pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); - li = lc_1 = eb = 0, bs = i; - for (var j = 0; j < 286; ++j) - lf[j] = 0; - for (var j = 0; j < 30; ++j) - df[j] = 0; - } - var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; - if (rem > 2 && hv === hsh(i - dif)) { - var maxn = Math.min(n, rem) - 1; - var maxd = Math.min(32767, i); - var ml = Math.min(258, rem); - while (dif <= maxd && --ch_1 && imod !== pimod) { - if (dat[i + l] === dat[i + l - dif]) { - var nl = 0; - for (; nl < ml && dat[i + nl] === dat[i + nl - dif]; ++nl) - ; - if (nl > l) { - l = nl, d = dif; - if (nl > maxn) - break; - var mmd = Math.min(dif, nl - 2); - var md = 0; - for (var j = 0; j < mmd; ++j) { - var ti = i - dif + j + 32768 & 32767; - var pti = prev[ti]; - var cd = ti - pti + 32768 & 32767; - if (cd > md) - md = cd, pimod = ti; - } - } - } - imod = pimod, pimod = prev[imod]; - dif += imod - pimod + 32768 & 32767; - } - } - if (d) { - syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; - var lin = revfl[l] & 31, din = revfd[d] & 31; - eb += fleb[lin] + fdeb[din]; - ++lf[257 + lin]; - ++df[din]; - wi = i + l; - ++lc_1; - } else { - syms[li++] = dat[i]; - ++lf[dat[i]]; - } - } - } - pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); - if (!lst && pos & 7) - pos = wfblk(w, pos + 1, et); - } - return slc(o, 0, pre + shft(pos) + post); -}; -var adler = function () { - var a = 1, b = 0; - return { - p: function (d) { - var n = a, m = b; - var l = d.length | 0; - for (var i = 0; i !== l;) { - var e = Math.min(i + 2655, l); - for (; i < e; ++i) - m += n += d[i]; - n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16); - } - a = n, b = m; - }, - d: function () { - a %= 65521, b %= 65521; - return (a & 255) << 24 | a >>> 8 << 16 | (b & 255) << 8 | b >>> 8; - } - }; -}; -var dopt = function (dat, opt, pre, post, st) { - return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 12 + opt.mem, pre, post, !st); -}; -var wbytes = function (d, b, v) { - for (; v; ++b) - d[b] = v, v >>>= 8; -}; -var zlh = function (c, o) { - var lv = o.level, fl2 = lv === 0 ? 0 : lv < 6 ? 1 : lv === 9 ? 3 : 2; - c[0] = 120, c[1] = fl2 << 6 | (fl2 ? 32 - 2 * fl2 : 1); -}; -var zlv = function (d) { - if ((d[0] & 15) !== 8 || d[0] >>> 4 > 7 || (d[0] << 8 | d[1]) % 31) - throw "invalid zlib data"; - if (d[1] & 32) - throw "invalid zlib data: preset dictionaries not supported"; -}; -function zlibSync(data, opts) { - if (!opts) - opts = {}; - var a = adler(); - a.p(data); - var d = dopt(data, opts, 2, 4); - return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; -} -function unzlibSync(data, out) { - return inflt((zlv(data), data.subarray(2, -4)), out); -} - -// bundle.js -export function compress(buf, level) { - return zlibSync(buf, { level }); -} - -export function decompress(buf, limit) { - try { return unzlibSync(buf, new Uint8Array(limit)); } - catch (err) { throw err.message ? err : new Error(err); } -} \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js new file mode 100644 index 0000000..6e0ea2e --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,36 @@ +// builds dist (ES modules and type declarations) from src +// CommonJS consumers load it through require(esm), available since Node 22.12 +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const root = path.join(__dirname, '..'); +const tsc = require.resolve('typescript/bin/tsc'); +const run = args => execFileSync(process.execPath, [tsc, ...args], { cwd: root, stdio: 'inherit' }); + +// keep the version reported in PNG metadata in sync with package.json +const { version } = require('../package.json'); +fs.writeFileSync(path.join(root, 'src/version.ts'), `// kept in sync with package.json by scripts/build.js\nexport const VERSION = '${version}';\n`); +// and the one JSR publishes +const jsrPath = path.join(root, 'jsr.json'); +const jsr = JSON.parse(fs.readFileSync(jsrPath, 'utf8')); +if (jsr.version !== version) fs.writeFileSync(jsrPath, JSON.stringify({ ...jsr, version }, null, 2) + '\n'); + +fs.rmSync(path.join(root, 'dist'), { recursive: true, force: true }); +// declarations keep the doc comments (editors show them), the JavaScript doesn't need them +run(['-p', 'tsconfig.json', '--emitDeclarationOnly']); +run(['-p', 'tsconfig.json', '--declaration', 'false', '--removeComments']); + +// declarations keep the .ts import specifiers of the sources, point them at the built files +const rewrite = dir => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const file = path.join(dir, entry.name); + if (entry.isDirectory()) rewrite(file); + else if (file.endsWith('.d.ts')) + fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/(from '\.{1,2}\/[^']*)\.ts'/g, "$1.js'")); + } +}; +rewrite(path.join(root, 'dist')); + +// the package itself has no "type", so mark the build as ES modules +fs.writeFileSync(path.join(root, 'dist/package.json'), '{"type":"module"}\n'); diff --git a/scripts/docs.js b/scripts/docs.js new file mode 100644 index 0000000..a3540fa --- /dev/null +++ b/scripts/docs.js @@ -0,0 +1,34 @@ +// builds the documentation site into docs/ with typedoc, plus redirects from the pages of the old jsdoc site +// (imagescript.matmen.dev/Image.html#resize -> classes/Image.html#resize) so existing links keep working +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const root = path.join(__dirname, '..'); +const out = path.join(root, 'docs'); +const typedoc = path.join(path.dirname(require.resolve('typedoc/package.json')), 'bin/typedoc'); + +fs.rmSync(out, { recursive: true, force: true }); +execFileSync(process.execPath, [typedoc], { cwd: root, stdio: 'inherit' }); + +// jsdoc anchors were the member names, prefixed with '.' for static members; typedoc lowercases them. +// global.html only documented decode(), which has a page of its own now +const redirects = { + 'Image.html': ['classes/Image.html', true], + 'Frame.html': ['classes/Frame.html', true], + 'GIF.html': ['classes/GIF.html', true], + 'TextLayout.html': ['classes/TextLayout.html', true], + 'global.html': ['functions/decode.html', false] +}; + +for (const [from, [to, keepAnchor]] of Object.entries(redirects)) { + const anchor = keepAnchor ? ` + location.hash.replace(/^#+\\.?/, '#').toLowerCase()` : ''; + fs.writeFileSync(path.join(out, from), ` + +Moved + + + +

This page moved to ${to}.

+`); +} diff --git a/scripts/release-check.js b/scripts/release-check.js new file mode 100644 index 0000000..35416eb --- /dev/null +++ b/scripts/release-check.js @@ -0,0 +1,109 @@ +// checks a GitHub release before it is published to npm and JSR, run by .github/workflows/release.yml: +// the tag matches package.json and jsr.json, the version is valid semver, newer than what npm has, not on JSR yet and +// listed in the changelog, prereleases are marked as such. Prints the npm dist-tag to publish under (and writes it to GITHUB_OUTPUT) +// usage: node scripts/release-check.js [prerelease flag of the GitHub release: true/false] +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '..'); +const { name, version } = require('../package.json'); +const jsr = require('../jsr.json'); +const [tag = '', prereleaseFlag = 'false'] = process.argv.slice(2); + +const errors = []; +const fail = message => errors.push(message); + +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; + +function parse(v) { + const m = SEMVER.exec(v); + return m && { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] ? m[4].split('.') : [] }; +} + +// semver precedence: -1, 0 or 1 +function compare(a, b) { + for (const k of ['major', 'minor', 'patch']) if (a[k] !== b[k]) return a[k] < b[k] ? -1 : 1; + // a prerelease comes before its release + if (!a.pre.length && !b.pre.length) return 0; + if (!a.pre.length) return 1; + if (!b.pre.length) return -1; + for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) { + const x = a.pre[i], y = b.pre[i]; + if (x === undefined) return -1; + if (y === undefined) return 1; + if (x === y) continue; + const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y); + if (nx && ny) return +x < +y ? -1 : 1; + if (nx !== ny) return nx ? -1 : 1; + return x < y ? -1 : 1; + } + return 0; +} + +async function main() { + const current = parse(version); + if (!current) fail(`package.json version ${version} is not valid semver`); + if (tag !== `v${version}`) fail(`release tag ${tag || '(none)'} doesn't match package.json, expected v${version}`); + + const prerelease = prereleaseFlag === 'true'; + if (current && prerelease !== current.pre.length > 0) + fail(current.pre.length ? `${version} is a prerelease version, mark the GitHub release as prerelease` + : `the GitHub release is marked as prerelease, but ${version} has no prerelease suffix (like -beta.1)`); + + if (jsr.version !== version) fail(`jsr.json has version ${jsr.version}, package.json ${version} (npm run build syncs them)`); + + const changelog = fs.readFileSync(path.join(root, 'CHANGELOG.md'), 'utf8'); + if (!changelog.split('\n').some(line => /^##\s/.test(line) && line.replace(/^##\s+v?/, '').trim().split(/\s/)[0] === version)) + fail(`CHANGELOG.md has no "## ${version}" section`); + + // what npm has already + const res = await fetch(`https://registry.npmjs.org/${name}`, { headers: { accept: 'application/vnd.npm.install-v1+json' } }); + if (!res.ok && res.status !== 404) throw new Error(`npm registry answered ${res.status}`); + const meta = res.ok ? await res.json() : { versions: {}, 'dist-tags': {} }; + const published = Object.keys(meta.versions).map(v => [v, parse(v)]).filter(([, p]) => p); + const latest = meta['dist-tags'].latest && parse(meta['dist-tags'].latest); + + let distTag = 'latest'; + if (current) { + const sameMajor = published.filter(([, p]) => p.major === current.major); + const newest = sameMajor.reduce((a, b) => !a || compare(b[1], a[1]) > 0 ? b : a, null); + if (meta.versions[version]) fail(`${name}@${version} is already published`); + else if (newest && compare(current, newest[1]) <= 0) + fail(`${version} isn't newer than ${newest[0]}, the newest published ${current.major}.x version`); + + // a skipped version is allowed (2.x was skipped on purpose), but likely a typo + const base = newest ? newest[1] : latest; + if (base && !meta.versions[version] && !current.pre.length && !base.pre.length) { + const next = [`${base.major}.${base.minor}.${base.patch + 1}`, `${base.major}.${base.minor + 1}.0`, `${base.major + 1}.0.0`]; + const { major, minor, patch } = current; + if (!next.includes(`${major}.${minor}.${patch}`)) + console.log(`::warning::${version} skips versions, the next ones after ${newest ? newest[0] : meta['dist-tags'].latest} would be ${next.join(', ')}`); + } + + // prereleases never become latest (3.1.0-beta.1 goes to "beta"), neither do fixes for older major versions + if (current.pre.length) distTag = /^[a-z]+$/i.test(current.pre[0]) ? current.pre[0].toLowerCase() : 'next'; + else if (latest && compare(current, latest) < 0) distTag = `v${current.major}-latest`; + } + + // JSR has no dist-tags, its "latest" is the newest stable version + const [scope, jsrName] = jsr.name.slice(1).split('/'); + const jsrRes = await fetch(`https://api.jsr.io/scopes/${scope}/packages/${jsrName}/versions/${encodeURIComponent(version)}`); + if (jsrRes.ok) fail(`${jsr.name}@${version} is already published on JSR`); + else if (jsrRes.status !== 404) throw new Error(`JSR answered ${jsrRes.status}`); + + if (errors.length) { + for (const e of errors) console.error(`::error::${e}`); + process.exit(1); + } + + console.log(`${name}@${version} can be published under the dist-tag "${distTag}"`); + if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `dist-tag=${distTag}\n`); +} + +if (require.main === module) main().catch(e => { + console.error(`::error::${e.message}`); + process.exit(1); +}); + +module.exports = { parse, compare }; diff --git a/src/codecs/crc.ts b/src/codecs/crc.ts new file mode 100644 index 0000000..5d32f9e --- /dev/null +++ b/src/codecs/crc.ts @@ -0,0 +1,33 @@ +/*! + * Slice-by-16 CRC-32, derived from "Fast CRC32 in JavaScript" (https://gist.github.com/101arrowz/e58695f7ccfdf74f60ba22018093edea) + * Copyright (c) 2021 Arjun Barrett, MIT License (see THIRD_PARTY_NOTICES.md) + */ + +// slice-by-16 tables: TABLE[k * 256 + n] is the CRC of byte n followed by k zero bytes +const TABLE = new Int32Array(16 * 256); +for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ c >>> 1 : c >>> 1; + TABLE[n] = c; +} +for (let n = 0; n < 256; n++) { + let c = TABLE[n]; + for (let k = 256; k < TABLE.length; k += 256) c = TABLE[k + n] = TABLE[c & 0xff] ^ c >>> 8; +} + +/** + * CRC-32 (ISO 3309), as used by PNG and zlib. Processes 16 bytes per step (slice-by-16, see #12), byte by byte so + * it doesn't depend on alignment or endianness + */ +export function crc32(buffer: Uint8Array): number { + let crc = -1, i = 0; + for (const end = buffer.length - 16; i <= end; i += 16) { + crc = TABLE[3840 + ((buffer[i] ^ crc) & 0xff)] ^ TABLE[3584 + ((buffer[i + 1] ^ crc >> 8) & 0xff)] + ^ TABLE[3328 + ((buffer[i + 2] ^ crc >> 16) & 0xff)] ^ TABLE[3072 + (buffer[i + 3] ^ crc >>> 24)] + ^ TABLE[2816 + buffer[i + 4]] ^ TABLE[2560 + buffer[i + 5]] ^ TABLE[2304 + buffer[i + 6]] ^ TABLE[2048 + buffer[i + 7]] + ^ TABLE[1792 + buffer[i + 8]] ^ TABLE[1536 + buffer[i + 9]] ^ TABLE[1280 + buffer[i + 10]] ^ TABLE[1024 + buffer[i + 11]] + ^ TABLE[768 + buffer[i + 12]] ^ TABLE[512 + buffer[i + 13]] ^ TABLE[256 + buffer[i + 14]] ^ TABLE[buffer[i + 15]]; + } + for (; i < buffer.length; i++) crc = TABLE[(crc ^ buffer[i]) & 0xff] ^ crc >>> 8; + return (crc ^ -1) >>> 0; +} diff --git a/src/codecs/exif.ts b/src/codecs/exif.ts new file mode 100644 index 0000000..c8b6666 --- /dev/null +++ b/src/codecs/exif.ts @@ -0,0 +1,38 @@ +/** + * Reads the orientation tag (0x0112) from the first IFD of Exif data + * @param data a TIFF header and IFDs, optionally preceded by "Exif\0\0" (as in JPEG's APP1) + * @return the orientation (1-8), 1 if it is missing or invalid + */ +export function orientation(data: Uint8Array): number { + if (data.length >= 6 && data[0] === 0x45 && data[1] === 0x78 && data[2] === 0x69 && data[3] === 0x66 && data[4] === 0 && data[5] === 0) + data = data.subarray(6); + if (data.length < 8) return 1; + + const tiff = new DataView(data.buffer, data.byteOffset, data.byteLength); + const little = tiff.getUint16(0) === 0x4949; + if (!little && tiff.getUint16(0) !== 0x4d4d) return 1; + + const ifd = tiff.getUint32(4, little); + if (ifd + 2 > tiff.byteLength) return 1; + + for (let i = 0, count = tiff.getUint16(ifd, little); i < count; i++) { + const entry = ifd + 2 + i * 12; + if (entry + 12 > tiff.byteLength) break; + if (tiff.getUint16(entry, little) !== 0x0112) continue; + + // a SHORT, stored in the first two bytes of the value field + const value = tiff.getUint16(entry + 8, little); + return value >= 1 && value <= 8 ? value : 1; + } + + return 1; +} + +/** + * Reads tiff:Orientation from an XMP packet, as attribute or element + * @return the orientation (1-8), 1 if it is missing or invalid + */ +export function xmpOrientation(xmp: string): number { + const match = /tiff:Orientation\s*(?:=\s*["']|>\s*)([1-8])\b/.exec(xmp); + return match ? +match[1] : 1; +} diff --git a/src/codecs/gif.ts b/src/codecs/gif.ts new file mode 100644 index 0000000..39bf3bd --- /dev/null +++ b/src/codecs/gif.ts @@ -0,0 +1,514 @@ +import { quantize } from './quantize.ts'; + +// ImageScript disposal modes (any, keep, previous, background) <-> GIF disposal methods (unspecified, none, background, previous) +const DISPOSAL_TO_GIF = [0, 1, 3, 2]; +const DISPOSAL_FROM_GIF = [0, 1, 3, 2, 0, 0, 0, 0]; + +const MAX_CODE = 4095; +const UNEXPECTED_END = new Error('GIF: unexpected end of data'); + +export interface GIFFrame { + x: number; + y: number; + width: number; + height: number; + /** Frame delay in centiseconds */ + delay: number; + /** ImageScript disposal mode (0 any, 1 keep, 2 previous, 3 background) */ + dispose: number; + /** RGBA pixel data */ + pixels: Uint8Array; +} + +export interface GIFData { + width: number; + height: number; + /** -1 loops forever, 0 plays once, n repeats n times */ + loopCount: number; + frames: GIFFrame[]; +} + +interface GraphicControl { + dispose: number; + delay: number; + transparentIndex: number; +} + +/** + * Encodes frames into a GIF + * @param {{width: number, height: number, loopCount?: number, frames: GIFFrame[]}} gif loopCount -1 loops forever, 0 plays once, n repeats n times + * @param {number} [quality=95] 1-100, scales the palette size (1 = 8 colors, 50 = 55, 90 and above = 256) + * @return {Uint8Array} + */ +export function encode({ width, height, loopCount = -1, frames }: Omit & { loopCount?: number }, quality = 95): Uint8Array { + const out = new ByteWriter(); + + out.bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61); // GIF89a + out.u16(width); + out.u16(height); + out.bytes(0x00, 0x00, 0x00); // no global color table, background 0, no aspect ratio + + if (loopCount !== 0) { + out.bytes(0x21, 0xff, 0x0b); + out.string('NETSCAPE2.0'); + out.bytes(0x03, 0x01); + out.u16(loopCount === -1 ? 0 : loopCount); + out.bytes(0x00); + } + + for (const frame of frames) { + const { palette, indices, transparentIndex } = quantize(frame.pixels, frame.width, { quality }); + const entries = palette.length / 3; + const bits = Math.max(1, Math.ceil(Math.log2(entries))); + + // graphic control extension + out.bytes(0x21, 0xf9, 0x04); + out.bytes(DISPOSAL_TO_GIF[frame.dispose] << 2 | (transparentIndex === -1 ? 0 : 1)); + out.u16(Math.min(0xffff, Math.max(0, Math.round(frame.delay)))); + out.bytes(transparentIndex === -1 ? 0 : transparentIndex, 0x00); + + // image descriptor with local color table + out.bytes(0x2c); + out.u16(frame.x); + out.u16(frame.y); + out.u16(frame.width); + out.u16(frame.height); + out.bytes(0x80 | bits - 1); + + const table = new Uint8Array(3 << bits); + table.set(palette); + out.array(table); + + const minCodeSize = Math.max(2, bits); + out.bytes(minCodeSize); + writeSubBlocks(out, lzwEncode(indices, minCodeSize)); + } + + out.bytes(0x3b); + return out.finish(); +} + +function lzwEncode(indices: Uint8Array, minCodeSize: number): Uint8Array { + const out = new ByteWriter(indices.length); + const clear = 1 << minCodeSize; + const eoi = clear + 1; + + // dictionary keyed by (prefix code << 8 | next index) + const dictionary = new Int16Array(4096 * 256); + + let codeSize = 0, next = 0, buffer = 0, bufferBits = 0; + const emit = (code: number): void => { + buffer |= code << bufferBits; + bufferBits += codeSize; + while (bufferBits >= 8) { + out.byte(buffer & 0xff); + buffer >>>= 8; + bufferBits -= 8; + } + }; + + const reset = () => { + dictionary.fill(0); + codeSize = minCodeSize + 1; + next = eoi + 1; + }; + + reset(); + emit(clear); + if (!indices.length) { + emit(eoi); + if (bufferBits) out.byte(buffer & 0xff); + return out.finish(); + } + + let prefix = indices[0]; + for (let i = 1; i < indices.length; i++) { + const index = indices[i]; + const key = prefix << 8 | index; + const code = dictionary[key]; + + if (code) { + prefix = code; + continue; + } + + emit(prefix); + + if (next <= MAX_CODE) { + dictionary[key] = next; + if (next === 1 << codeSize && codeSize < 12) codeSize++; + next++; + } else { + emit(clear); + reset(); + } + + prefix = index; + } + + emit(prefix); + emit(eoi); + if (bufferBits) out.byte(buffer & 0xff); + + return out.finish(); +} + +function writeSubBlocks(out: ByteWriter, data: Uint8Array): void { + for (let i = 0; i < data.length; i += 255) { + const block = data.subarray(i, i + 255); + out.byte(block.length); + out.array(block); + } + + out.byte(0x00); +} + +/** + * Decodes a GIF into its individual (uncomposited) frames, zero-sized images are skipped + * @param {Uint8Array} data + * @param {boolean} [onlyFirstFrame=false] + * @return {{width: number, height: number, loopCount: number, frames: GIFFrame[]}} + */ +export function decode(data: Uint8Array, onlyFirstFrame = false): GIFData { + const reader = new ByteReader(data); + + const signature = reader.string(6); + if (signature !== 'GIF87a' && signature !== 'GIF89a') throw new Error('GIF: invalid signature'); + + const width = reader.u16(); + const height = reader.u16(); + const flags = reader.u8(); + reader.skip(2); // background color index, aspect ratio + + const globalTable = flags & 0x80 ? reader.slice(3 << (flags & 7) + 1) : null; + + const frames: GIFFrame[] = []; + let loopCount = 0; + let control: GraphicControl | null = null; + + try { + parse(); + } catch (err) { + // truncated files keep every frame decoded so far, like browsers do + if (err !== UNEXPECTED_END) throw err; + } + + return { width, height, loopCount, frames }; + + function parse(): void { + while (reader.offset < data.length) { + const block = reader.u8(); + + if (block === 0x3b) break; // trailer + + if (block === 0x21) { + const label = reader.u8(); + + if (label === 0xf9) { + const size = reader.u8(); + const packed = reader.u8(); + control = { + dispose: DISPOSAL_FROM_GIF[packed >> 2 & 7], + delay: reader.u16(), + transparentIndex: packed & 1 ? reader.u8() : (reader.skip(1), -1) + }; + reader.skip(size - 4); + skipSubBlocks(reader); + } else if (label === 0xff) { + const size = reader.u8(); + const id = reader.string(size); + + if (id === 'NETSCAPE2.0' || id === 'ANIMEXTS1.0') { + // sub-block 1 holds the loop count + for (let length; (length = reader.u8());) { + const sub = reader.slice(length); + if (sub[0] === 1 && length >= 3) { + const count = sub[1] | sub[2] << 8; + loopCount = count === 0 ? -1 : count; + } + } + } else skipSubBlocks(reader); + } else skipSubBlocks(reader); + + continue; + } + + if (block !== 0x2c) { + // unknown block, stop gracefully with whatever frames were decoded + if (frames.length) break; + throw new Error(`GIF: unknown block type 0x${block.toString(16)}`); + } + + const x = reader.u16(); + const y = reader.u16(); + const fw = reader.u16(); + const fh = reader.u16(); + const descriptor = reader.u8(); + const localTable = descriptor & 0x80 ? reader.slice(3 << (descriptor & 7) + 1) : null; + const interlaced = !!(descriptor & 0x40); + const table = localTable || globalTable; + + const minCodeSize = reader.u8(); + const compressed = readSubBlocks(reader); + const indices = lzwDecode(compressed, minCodeSize, fw * fh); + + const { dispose = 0, delay = 0, transparentIndex = -1 } = control || {}; + control = null; + if (!fw || !fh) continue; + + const pixels = new Uint8Array(fw * fh * 4); + const rows = interlaced ? interlacedRows(fh) : null; + + for (let row = 0; row < fh; row++) { + const targetRow = rows ? rows[row] : row; + + for (let column = 0; column < fw; column++) { + const index = indices[row * fw + column]; + if (index === transparentIndex) continue; + + const o = (targetRow * fw + column) * 4; + if (table && index * 3 + 2 < table.length) { + pixels[o] = table[index * 3]; + pixels[o + 1] = table[index * 3 + 1]; + pixels[o + 2] = table[index * 3 + 2]; + } + pixels[o + 3] = 0xff; + } + } + + frames.push({ x, y, width: fw, height: fh, delay, dispose, pixels }); + if (onlyFirstFrame) break; + } + } +} + +/** + * Composites decoded frames onto the logical screen, applying their disposal modes + * @param {{width: number, height: number, frames: GIFFrame[]}} gif output of {@link decode} + * @return {Array<{pixels: Uint8Array, delay: number}>} full screen RGBA frames + */ +export function composite({ width, height, frames }: GIFData): Array<{ pixels: Uint8Array, delay: number }> { + const canvas = new Uint32Array(width * height); + const output = []; + + for (const frame of frames) { + const previous = frame.dispose === 2 ? canvas.slice() : null; + const src = new Uint32Array(frame.pixels.buffer, frame.pixels.byteOffset, frame.width * frame.height); + + // clip the frame to the logical screen + const x0 = Math.min(frame.x, width), x1 = Math.min(frame.x + frame.width, width); + const y0 = Math.min(frame.y, height), y1 = Math.min(frame.y + frame.height, height); + + for (let y = y0; y < y1; y++) { + const srcRow = (y - frame.y) * frame.width - frame.x; + const dstRow = y * width; + + for (let x = x0; x < x1; x++) + if (frame.pixels[(srcRow + x) * 4 + 3] !== 0) canvas[dstRow + x] = src[srcRow + x]; + } + + output.push({ pixels: new Uint8Array(canvas.slice().buffer), delay: frame.delay }); + + if (frame.dispose === 3) { + for (let y = y0; y < y1; y++) canvas.fill(0, y * width + x0, y * width + x1); + } else if (previous) canvas.set(previous); + } + + // a GIF without image data still describes a (transparent) canvas + if (!output.length) output.push({ pixels: new Uint8Array(width * height * 4), delay: 0 }); + return output; +} + +// maps the n-th stored row of an interlaced image to its actual row +function interlacedRows(height: number): number[] { + const rows = []; + for (const [start, step] of [[0, 8], [4, 8], [2, 4], [1, 2]]) + for (let y = start; y < height; y += step) rows.push(y); + return rows; +} + +function lzwDecode(data: Uint8Array, minCodeSize: number, pixelCount: number): Uint8Array { + const output = new Uint8Array(pixelCount); + if (minCodeSize < 1 || minCodeSize > 11) return output; + + const clear = 1 << minCodeSize; + const eoi = clear + 1; + + // each code is stored as (prefix code, suffix byte, first byte, length) + const prefix = new Uint16Array(4096); + const suffix = new Uint8Array(4096); + const first = new Uint8Array(4096); + const lengths = new Uint16Array(4096); + for (let i = 0; i < clear; i++) { + suffix[i] = first[i] = i; + lengths[i] = 1; + } + + let codeSize = minCodeSize + 1; + let next = eoi + 1; + let previous = -1; + let buffer = 0, bufferBits = 0, position = 0, out = 0; + + while (out < pixelCount) { + while (bufferBits < codeSize) { + if (position >= data.length) return output; // truncated data, leave the remainder at index 0 + buffer |= data[position++] << bufferBits; + bufferBits += 8; + } + + const code = buffer & (1 << codeSize) - 1; + buffer >>>= codeSize; + bufferBits -= codeSize; + + if (code === clear) { + codeSize = minCodeSize + 1; + next = eoi + 1; + previous = -1; + continue; + } + + if (code === eoi) break; + + let entry; + if (code < next) entry = code; + else if (code === next && previous !== -1) entry = previous; + else break; // invalid code + + if (previous === -1 && code >= clear) break; + + // write the string for `entry`, backwards from its end + const length = lengths[entry]; + const end = Math.min(out + length, pixelCount); + let c = entry; + for (let i = out + length - 1; i >= out; i--) { + if (i < end) output[i] = suffix[c]; + c = prefix[c]; + } + + let written = end - out; + if (code === next) { + // KwKwK case: the new string is previous + first byte of previous + if (out + length < pixelCount) { + output[out + length] = first[previous]; + written++; + } + } + out += written; + + if (previous !== -1 && next <= MAX_CODE) { + prefix[next] = previous; + suffix[next] = first[code === next ? previous : code]; + first[next] = first[previous]; + lengths[next] = lengths[previous] + 1; + next++; + if (next === 1 << codeSize && codeSize < 12) codeSize++; + } + + previous = code; + } + + return output; +} + +function readSubBlocks(reader: ByteReader): Uint8Array { + const blocks = []; + let total = 0; + + for (let length; reader.offset < reader.data.length && (length = reader.u8());) { + const block = reader.slice(length); + blocks.push(block); + total += block.length; + } + + const out = new Uint8Array(total); + let offset = 0; + for (const block of blocks) { + out.set(block, offset); + offset += block.length; + } + + return out; +} + +function skipSubBlocks(reader: ByteReader): void { + for (let length; reader.offset < reader.data.length && (length = reader.u8());) reader.skip(length); +} + +class ByteReader { + data: Uint8Array; + offset: number; + + constructor(data: Uint8Array) { + this.data = data; + this.offset = 0; + } + + u8() { + if (this.offset >= this.data.length) throw UNEXPECTED_END; + return this.data[this.offset++]; + } + + u16() { + return this.u8() | this.u8() << 8; + } + + skip(length: number): void { + this.offset += length; + } + + slice(length: number): Uint8Array { + const slice = this.data.subarray(this.offset, this.offset + length); + this.offset += length; + return slice; + } + + string(length: number): string { + return String.fromCharCode(...this.slice(length)); + } +} + +class ByteWriter { + buffer: Uint8Array; + length: number; + + constructor(capacity = 1024) { + this.buffer = new Uint8Array(Math.max(16, capacity)); + this.length = 0; + } + + reserve(size: number): void { + if (this.length + size <= this.buffer.length) return; + const buffer = new Uint8Array(Math.max(this.buffer.length * 2, this.length + size)); + buffer.set(this.buffer.subarray(0, this.length)); + this.buffer = buffer; + } + + byte(value: number): void { + this.reserve(1); + this.buffer[this.length++] = value; + } + + bytes(...values: number[]): void { + for (const value of values) this.byte(value); + } + + u16(value: number): void { + this.byte(value & 0xff); + this.byte(value >> 8 & 0xff); + } + + string(value: string): void { + for (let i = 0; i < value.length; i++) this.byte(value.charCodeAt(i)); + } + + array(values: ArrayLike): void { + this.reserve(values.length); + this.buffer.set(values, this.length); + this.length += values.length; + } + + finish(): Uint8Array { + return this.buffer.slice(0, this.length); + } +} + diff --git a/src/codecs/jpeg.ts b/src/codecs/jpeg.ts new file mode 100644 index 0000000..fbb8fe7 --- /dev/null +++ b/src/codecs/jpeg.ts @@ -0,0 +1,1215 @@ +/*! + * JPEG codec. This software is based in part on the work of the Independent JPEG Group: the IDCT, FDCT, upsampling, + * color conversion, progressive refinement and optimal Huffman tables are translated from libjpeg/libjpeg-turbo + * (IJG license, see licenses/README.ijg). The scan decoder follows the structure of pdf.js' jpg.js + * (Copyright 2014 Mozilla Foundation, Apache License 2.0). See THIRD_PARTY_NOTICES.md + */ + +import { orientation as exifOrientation } from './exif.ts'; + +// natural order index of the n-th zigzag coefficient +const ZIGZAG = Int32Array.of( + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63 +); + +type Transform = 'grey' | 'rgb' | 'ycc' | 'ycck' | 'cmyk'; + +interface HuffmanTable { + /** (length << 8 | symbol) for codes up to 9 bits, 0 = slow path */ + lookup: Uint16Array; + maxCode: Int32Array; + valueOffset: Int32Array; + values: Uint8Array; +} + +interface Component { + id: number; + h: number; + v: number; + tq: number; + /** dimensions of the component's sample plane */ + width: number; + height: number; + blocksPerLine: number; + blocksPerColumn: number; + /** blocks are allocated for whole MCUs */ + blocksPerLineForMcu: number; + blocksPerColumnForMcu: number; + coefficients: Int16Array; + pred: number; + dcTable?: HuffmanTable; + acTable?: HuffmanTable; + quantization?: Int32Array; +} + +interface Frame { + progressive: boolean; + width: number; + height: number; + components: Component[]; + hmax: number; + vmax: number; + mcusPerLine: number; + mcusPerColumn: number; +} + +export interface DecodeOptions { + /** overrides the color transform detected from JFIF/Adobe markers (used by TIFF) */ + transform?: 'ycc' | 'rgb' | 'cmyk' | null; +} + +function clamp8(v: number): number { + return v < 0 ? 0 : v > 255 ? 255 : v; +} + +// --------------------------------------------------------------------------- +// decoder +// --------------------------------------------------------------------------- + +/** + * Decodes a JPEG into 8-bit RGBA pixel data + * Supports baseline, extended and progressive huffman coded JPEGs with any + * sampling factors, restart intervals and greyscale, YCbCr, RGB, CMYK and YCCK color + * The output matches libjpeg's defaults (islow IDCT, fancy upsampling) + * @param {Uint8Array} data + * @param {{transform?: 'ycc'|'rgb'|'cmyk'}} [options] overrides the color transform detected from JFIF/Adobe markers (used by TIFF) + * @return {{width: number, height: number, pixels: Uint8Array}} + */ +export function decode(data: Uint8Array, { transform }: DecodeOptions = {}): { width: number, height: number, pixels: Uint8Array, orientation: number } { + if (data[0] !== 0xff || data[1] !== 0xd8) throw new Error('JPEG: invalid signature'); + + const quantizationTables: Int32Array[] = []; + const dcTables: HuffmanTable[] = []; + const acTables: HuffmanTable[] = []; + let frame: Frame | null = null; + let adobe: { transform: number } | null = null; + let jfif = false; + let orientation = 1; + let restartInterval = 0; + let pos = 2; + + const u16 = (offset: number): number => data[offset] << 8 | data[offset + 1]; + + while (pos < data.length) { + // find the next marker, skipping fill bytes and stray data + if (data[pos] !== 0xff) { + pos++; + continue; + } + + const marker = data[pos + 1]; + if (marker === 0xff || marker === 0x00 || marker === undefined) { + pos++; + continue; + } + + pos += 2; + + if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) continue; + if (marker === 0xd9) break; + + const length = u16(pos); + const start = pos + 2; + const end = pos + length; + if (end > data.length) throw new Error('JPEG: unexpected end of data'); + + switch (marker) { + case 0xdb: { // DQT + let p = start; + while (p < end) { + const precision = data[p] >> 4; + const id = data[p++] & 15; + const table = new Int32Array(64); + + for (let k = 0; k < 64; k++) { + table[ZIGZAG[k]] = precision ? u16(p) : data[p]; + p += precision ? 2 : 1; + } + + quantizationTables[id] = table; + } + break; + } + + case 0xc4: { // DHT + let p = start; + while (p < end) { + const tableClass = data[p] >> 4; + const id = data[p++] & 15; + const counts = data.subarray(p, p + 16); + p += 16; + + let total = 0; + for (let i = 0; i < 16; i++) total += counts[i]; + + const table = buildHuffmanTable(counts, data.subarray(p, p + total)); + p += total; + (tableClass === 0 ? dcTables : acTables)[id] = table; + } + break; + } + + case 0xdd: // DRI + restartInterval = u16(start); + break; + + case 0xc0: case 0xc1: case 0xc2: { // SOF baseline, extended, progressive + if (frame) throw new Error('JPEG: multiple frames are not supported'); + if (data[start] !== 8) throw new Error(`JPEG: ${data[start]}-bit precision is not supported`); + frame = createFrame(marker === 0xc2, u16(start + 1), u16(start + 3), data.subarray(start + 5, end)); + break; + } + + case 0xc3: case 0xc5: case 0xc6: case 0xc7: + case 0xc9: case 0xca: case 0xcb: case 0xcd: case 0xce: case 0xcf: + throw new Error(marker === 0xc3 || marker === 0xc7 || marker === 0xcb || marker === 0xcf + ? 'JPEG: lossless JPEGs are not supported' + : marker >= 0xc9 ? 'JPEG: arithmetic coding is not supported' : 'JPEG: hierarchical JPEGs are not supported'); + + case 0xe0: // APP0 + if (data[start] === 0x4a && data[start + 1] === 0x46 && data[start + 2] === 0x49 && data[start + 3] === 0x46 && data[start + 4] === 0) jfif = true; + break; + + case 0xe1: // APP1, the first Exif block holds the orientation + if (orientation === 1) orientation = exifOrientation(data.subarray(start, end)); + break; + + case 0xee: // APP14 + if (length >= 14 && data[start] === 0x41 && data[start + 1] === 0x64 && data[start + 2] === 0x6f && data[start + 3] === 0x62 && data[start + 4] === 0x65) + adobe = { transform: data[start + 11] }; + break; + + case 0xda: { // SOS + if (!frame) throw new Error('JPEG: scan before frame header'); + if (frame.height === 0) throw new Error('JPEG: DNL markers are not supported'); + + const count = data[start]; + const components = []; + let p = start + 1; + + for (let i = 0; i < count; i++) { + const component = frame.components.find(c => c.id === data[p]); + if (!component) throw new Error('JPEG: scan references unknown component'); + + component.dcTable = dcTables[data[p + 1] >> 4]; + component.acTable = acTables[data[p + 1] & 15]; + // latch the quantization table on first use, it may be redefined afterwards + if (!component.quantization) { + const table = quantizationTables[component.tq]; + if (!table) throw new Error('JPEG: missing quantization table'); + component.quantization = table.slice(); + } + + components.push(component); + p += 2; + } + + const spectralStart = data[p]; + const spectralEnd = data[p + 1]; + const successiveHigh = data[p + 2] >> 4; + const successiveLow = data[p + 2] & 15; + + pos = decodeScan(data, end, frame, components, restartInterval, spectralStart, spectralEnd, successiveHigh, successiveLow); + continue; + } + } + + pos = end; + } + + if (!frame) throw new Error('JPEG: missing frame header'); + return { width: frame.width, height: frame.height, pixels: output(frame, adobe, jfif, transform), orientation }; +} + +function createFrame(progressive: boolean, height: number, width: number, componentData: Uint8Array): Frame { + const count = componentData[0]; + const sampling: Array<{ id: number, h: number, v: number, tq: number }> = []; + + for (let i = 0; i < count; i++) { + const p = 1 + i * 3; + sampling.push({ id: componentData[p], h: componentData[p + 1] >> 4 || 1, v: componentData[p + 1] & 15 || 1, tq: componentData[p + 2] }); + } + + if (!width || !sampling.length) throw new Error('JPEG: invalid dimensions'); + + const hmax = Math.max(...sampling.map(c => c.h)); + const vmax = Math.max(...sampling.map(c => c.v)); + const mcusPerLine = Math.ceil(width / (8 * hmax)); + const mcusPerColumn = Math.ceil(height / (8 * vmax)); + + const components = sampling.map((c): Component => { + const componentWidth = Math.ceil(width * c.h / hmax); + const componentHeight = Math.ceil(height * c.v / vmax); + const blocksPerLineForMcu = mcusPerLine * c.h; + const blocksPerColumnForMcu = mcusPerColumn * c.v; + + return { + ...c, + width: componentWidth, + height: componentHeight, + blocksPerLine: Math.ceil(componentWidth / 8), + blocksPerColumn: Math.ceil(componentHeight / 8), + blocksPerLineForMcu, + blocksPerColumnForMcu, + coefficients: new Int16Array(blocksPerLineForMcu * blocksPerColumnForMcu * 64), + pred: 0 + }; + }); + + return { progressive, width, height, components, hmax, vmax, mcusPerLine, mcusPerColumn }; +} + +function buildHuffmanTable(counts: Uint8Array, symbols: Uint8Array): HuffmanTable { + // fast lookup for codes up to 9 bits: (length << 8 | symbol), 0 = slow path + const lookup = new Uint16Array(512); + const maxCode = new Int32Array(18).fill(-1); + const valueOffset = new Int32Array(17); + const values = Uint8Array.from(symbols); + + let code = 0, k = 0; + for (let length = 1; length <= 16; length++) { + valueOffset[length] = k - code; + + for (let i = 0; i < counts[length - 1]; i++, k++, code++) { + if (length <= 9) { + const shift = 9 - length; + for (let j = 0; j < 1 << shift; j++) lookup[code << shift | j] = length << 8 | values[k]; + } + } + + if (counts[length - 1]) maxCode[length] = code - 1; + code <<= 1; + } + + maxCode[17] = 0x7fffffff; + return { lookup, maxCode, valueOffset, values }; +} + +function decodeScan(data: Uint8Array, offset: number, frame: Frame, components: Component[], restartInterval: number, ss: number, se: number, ah: number, al: number): number { + // bit reader state + let pos = offset; + let buffer = 0, bits = 0; + + const fill = (): void => { + while (bits <= 24) { + let byte = 0; + if (pos < data.length) { + byte = data[pos]; + if (byte === 0xff) { + const next = data[pos + 1]; + if (next === 0x00) pos += 2; + else byte = 0; // marker reached, feed zeros + } else pos++; + } + + buffer = buffer << 8 | byte; + bits += 8; + } + }; + + const getBits = (n: number): number => { + if (bits < n) fill(); + bits -= n; + return buffer >>> bits & (1 << n) - 1; + }; + + const getBit = (): number => { + if (bits < 1) fill(); + return buffer >>> --bits & 1; + }; + + const huffman = (table: HuffmanTable | undefined): number => { + if (!table) throw new Error('JPEG: missing huffman table'); + if (bits < 16) fill(); + const entry = table.lookup[buffer >>> bits - 9 & 511]; + + if (entry) { + bits -= entry >> 8; + return entry & 255; + } + + let length = 10; + let code = buffer >>> bits - 10 & 1023; + while (code > table.maxCode[length]) { + if (++length > 16) throw new Error('JPEG: invalid huffman code'); + code = buffer >>> bits - length & (1 << length) - 1; + } + + bits -= length; + return table.values[code + table.valueOffset[length]]; + }; + + const extend = (value: number, size: number): number => value < 1 << size - 1 ? value + (-1 << size) + 1 : value; + const receiveExtend = (size: number): number => size ? extend(getBits(size), size) : 0; + + let eobrun = 0; + + const decodeBaseline = (component: Component, offset: number): void => { + const coefficients = component.coefficients; + const t = huffman(component.dcTable); + coefficients[offset] = component.pred += receiveExtend(t); + + for (let k = 1; k < 64;) { + const rs = huffman(component.acTable); + const s = rs & 15, r = rs >> 4; + + if (s === 0) { + if (r < 15) break; + k += 16; + continue; + } + + k += r; + if (k > 63) break; + coefficients[offset + ZIGZAG[k++]] = receiveExtend(s); + } + }; + + const decodeDCFirst = (component: Component, offset: number): void => { + const t = huffman(component.dcTable); + component.coefficients[offset] = (component.pred += receiveExtend(t)) * (1 << al); + }; + + const decodeDCSuccessive = (component: Component, offset: number): void => { + if (getBit()) component.coefficients[offset] |= 1 << al; + }; + + const decodeACFirst = (component: Component, offset: number): void => { + if (eobrun > 0) { + eobrun--; + return; + } + + const coefficients = component.coefficients; + for (let k = ss; k <= se;) { + const rs = huffman(component.acTable); + const s = rs & 15, r = rs >> 4; + + if (s === 0) { + if (r < 15) { + eobrun = (1 << r) - 1; + if (r) eobrun += getBits(r); + break; + } + + k += 16; + continue; + } + + k += r; + if (k > 63) break; + coefficients[offset + ZIGZAG[k++]] = receiveExtend(s) * (1 << al); + } + }; + + const decodeACSuccessive = (component: Component, offset: number): void => { + const coefficients = component.coefficients; + const p1 = 1 << al; + const m1 = -1 << al; + let k = ss; + + if (eobrun === 0) { + for (; k <= se; k++) { + const rs = huffman(component.acTable); + let r = rs >> 4; + let s = rs & 15; + + if (s) s = getBit() ? p1 : m1; + else if (r !== 15) { + eobrun = 1 << r; + if (r) eobrun += getBits(r); + break; + } + + // skip r zero coefficients, refining the nonzero ones passed on the way + do { + const z = offset + ZIGZAG[k]; + const c = coefficients[z]; + + if (c !== 0) { + if (getBit() && (c & p1) === 0) coefficients[z] = c >= 0 ? c + p1 : c + m1; + } else if (--r < 0) break; + + k++; + } while (k <= se); + + if (s && k <= se) coefficients[offset + ZIGZAG[k]] = s; + } + } + + if (eobrun > 0) { + for (; k <= se; k++) { + const z = offset + ZIGZAG[k]; + const c = coefficients[z]; + if (c !== 0 && getBit() && (c & p1) === 0) coefficients[z] = c >= 0 ? c + p1 : c + m1; + } + + eobrun--; + } + }; + + const decodeBlock = !frame.progressive ? decodeBaseline + : ss === 0 ? (ah === 0 ? decodeDCFirst : decodeDCSuccessive) + : (ah === 0 ? decodeACFirst : decodeACSuccessive); + + const single = components.length === 1; + const total = single + ? components[0].blocksPerLine * components[0].blocksPerColumn + : frame.mcusPerLine * frame.mcusPerColumn; + const interval = restartInterval || total; + + for (const component of components) component.pred = 0; + + for (let unit = 0; unit < total;) { + for (let n = 0; n < interval && unit < total; n++, unit++) { + if (single) { + const component = components[0]; + const row = unit / component.blocksPerLine | 0; + const column = unit % component.blocksPerLine; + decodeBlock(component, (row * component.blocksPerLineForMcu + column) * 64); + } else { + const mcuRow = unit / frame.mcusPerLine | 0; + const mcuColumn = unit % frame.mcusPerLine; + + for (const component of components) + for (let v = 0; v < component.v; v++) + for (let h = 0; h < component.h; h++) { + const row = mcuRow * component.v + v; + const column = mcuColumn * component.h + h; + decodeBlock(component, (row * component.blocksPerLineForMcu + column) * 64); + } + } + } + + if (unit >= total) break; + + // restart: discard remaining bits and expect an RSTn marker + bits = 0; + buffer = 0; + while (pos + 1 < data.length && !(data[pos] === 0xff && data[pos + 1] >= 0xd0 && data[pos + 1] <= 0xd7)) { + // a different marker means the scan ended early + if (data[pos] === 0xff && data[pos + 1] !== 0x00 && data[pos + 1] !== 0xff) return pos; + pos++; + } + + if (pos + 1 >= data.length) return pos; + pos += 2; + eobrun = 0; + for (const component of components) component.pred = 0; + } + + return pos; +} + +// libjpeg's jpeg_idct_islow +const CONST_BITS = 13; +const PASS1_BITS = 2; +const FIX_0_298631336 = 2446; +const FIX_0_390180644 = 3196; +const FIX_0_541196100 = 4433; +const FIX_0_765366865 = 6270; +const FIX_0_899976223 = 7373; +const FIX_1_175875602 = 9633; +const FIX_1_501321110 = 12299; +const FIX_1_847759065 = 15137; +const FIX_1_961570560 = 16069; +const FIX_2_053119869 = 16819; +const FIX_2_562915447 = 20995; +const FIX_3_072711026 = 25172; + +function idct(coefficients: Int16Array, offset: number, quantization: Int32Array, workspace: Int32Array, out: Uint8Array, outOffset: number, stride: number): void { + const P1 = CONST_BITS - PASS1_BITS; + const R1 = 1 << P1 - 1; + + for (let column = 0; column < 8; column++) { + const c = offset + column; + + if (coefficients[c + 8] === 0 && coefficients[c + 16] === 0 && coefficients[c + 24] === 0 && coefficients[c + 32] === 0 + && coefficients[c + 40] === 0 && coefficients[c + 48] === 0 && coefficients[c + 56] === 0) { + const dc = coefficients[c] * quantization[column] << PASS1_BITS; + for (let row = 0; row < 64; row += 8) workspace[row + column] = dc; + continue; + } + + let z2 = coefficients[c + 16] * quantization[column + 16]; + let z3 = coefficients[c + 48] * quantization[column + 48]; + let z1 = (z2 + z3) * FIX_0_541196100; + let tmp2 = z1 - z3 * FIX_1_847759065; + let tmp3 = z1 + z2 * FIX_0_765366865; + + z2 = coefficients[c] * quantization[column]; + z3 = coefficients[c + 32] * quantization[column + 32]; + let tmp0 = z2 + z3 << CONST_BITS; + let tmp1 = z2 - z3 << CONST_BITS; + + const tmp10 = tmp0 + tmp3, tmp13 = tmp0 - tmp3; + const tmp11 = tmp1 + tmp2, tmp12 = tmp1 - tmp2; + + tmp0 = coefficients[c + 56] * quantization[column + 56]; + tmp1 = coefficients[c + 40] * quantization[column + 40]; + tmp2 = coefficients[c + 24] * quantization[column + 24]; + tmp3 = coefficients[c + 8] * quantization[column + 8]; + + z1 = tmp0 + tmp3; + z2 = tmp1 + tmp2; + z3 = tmp0 + tmp2; + let z4 = tmp1 + tmp3; + const z5 = (z3 + z4) * FIX_1_175875602; + + tmp0 *= FIX_0_298631336; + tmp1 *= FIX_2_053119869; + tmp2 *= FIX_3_072711026; + tmp3 *= FIX_1_501321110; + z1 *= -FIX_0_899976223; + z2 *= -FIX_2_562915447; + z3 = z3 * -FIX_1_961570560 + z5; + z4 = z4 * -FIX_0_390180644 + z5; + + tmp0 += z1 + z3; + tmp1 += z2 + z4; + tmp2 += z2 + z3; + tmp3 += z1 + z4; + + workspace[column] = tmp10 + tmp3 + R1 >> P1; + workspace[column + 56] = tmp10 - tmp3 + R1 >> P1; + workspace[column + 8] = tmp11 + tmp2 + R1 >> P1; + workspace[column + 48] = tmp11 - tmp2 + R1 >> P1; + workspace[column + 16] = tmp12 + tmp1 + R1 >> P1; + workspace[column + 40] = tmp12 - tmp1 + R1 >> P1; + workspace[column + 24] = tmp13 + tmp0 + R1 >> P1; + workspace[column + 32] = tmp13 - tmp0 + R1 >> P1; + } + + const P2 = CONST_BITS + PASS1_BITS + 3; + const R2 = 1 << P2 - 1; + + for (let row = 0; row < 64; row += 8) { + const o = outOffset + (row >> 3) * stride; + + let z2 = workspace[row + 2]; + let z3 = workspace[row + 6]; + let z1 = (z2 + z3) * FIX_0_541196100; + let tmp2 = z1 - z3 * FIX_1_847759065; + let tmp3 = z1 + z2 * FIX_0_765366865; + + let tmp0 = workspace[row] + workspace[row + 4] << CONST_BITS; + let tmp1 = workspace[row] - workspace[row + 4] << CONST_BITS; + + const tmp10 = tmp0 + tmp3, tmp13 = tmp0 - tmp3; + const tmp11 = tmp1 + tmp2, tmp12 = tmp1 - tmp2; + + tmp0 = workspace[row + 7]; + tmp1 = workspace[row + 5]; + tmp2 = workspace[row + 3]; + tmp3 = workspace[row + 1]; + + z1 = tmp0 + tmp3; + z2 = tmp1 + tmp2; + z3 = tmp0 + tmp2; + let z4 = tmp1 + tmp3; + const z5 = (z3 + z4) * FIX_1_175875602; + + tmp0 *= FIX_0_298631336; + tmp1 *= FIX_2_053119869; + tmp2 *= FIX_3_072711026; + tmp3 *= FIX_1_501321110; + z1 *= -FIX_0_899976223; + z2 *= -FIX_2_562915447; + z3 = z3 * -FIX_1_961570560 + z5; + z4 = z4 * -FIX_0_390180644 + z5; + + tmp0 += z1 + z3; + tmp1 += z2 + z4; + tmp2 += z2 + z3; + tmp3 += z1 + z4; + + out[o] = clamp8((tmp10 + tmp3 + R2 >> P2) + 128); + out[o + 7] = clamp8((tmp10 - tmp3 + R2 >> P2) + 128); + out[o + 1] = clamp8((tmp11 + tmp2 + R2 >> P2) + 128); + out[o + 6] = clamp8((tmp11 - tmp2 + R2 >> P2) + 128); + out[o + 2] = clamp8((tmp12 + tmp1 + R2 >> P2) + 128); + out[o + 5] = clamp8((tmp12 - tmp1 + R2 >> P2) + 128); + out[o + 3] = clamp8((tmp13 + tmp0 + R2 >> P2) + 128); + out[o + 4] = clamp8((tmp13 - tmp0 + R2 >> P2) + 128); + } +} + +// runs the IDCT over every block, producing the component's sample plane +function buildPlane(component: Component): { plane: Uint8Array, stride: number } { + const stride = component.blocksPerLineForMcu * 8; + const plane = new Uint8Array(stride * component.blocksPerColumnForMcu * 8); + const workspace = new Int32Array(64); + // components without any scan data decode to mid grey + const quantization = component.quantization || new Int32Array(64); + + for (let row = 0; row < component.blocksPerColumn; row++) + for (let column = 0; column < component.blocksPerLine; column++) + idct(component.coefficients, (row * component.blocksPerLineForMcu + column) * 64, quantization, workspace, plane, row * 8 * stride + column * 8, stride); + + return { plane, stride }; +} + +// upsamples a component to full resolution, following libjpeg's fancy upsampling +function upsample(component: Component, frame: Frame): { samples: Uint8Array, stride: number } { + const { plane, stride } = buildPlane(component); + const hf = frame.hmax / component.h; + const vf = frame.vmax / component.v; + const w = component.width, h = component.height; + const W = Math.ceil(w * hf), H = Math.ceil(h * vf); + const out = new Uint8Array(W * H); + const fancy = w > 2; + + if (hf === 1 && vf === 1) { + for (let y = 0; y < h; y++) out.set(plane.subarray(y * stride, y * stride + w), y * W); + } else if (hf === 2 && vf === 1 && fancy) { + for (let y = 0; y < h; y++) { + const i = y * stride, o = y * W; + out[o] = plane[i]; + out[o + 1] = plane[i] * 3 + plane[i + 1] + 2 >> 2; + for (let x = 1; x < w - 1; x++) { + const v = plane[i + x] * 3; + out[o + 2 * x] = v + plane[i + x - 1] + 1 >> 2; + out[o + 2 * x + 1] = v + plane[i + x + 1] + 2 >> 2; + } + out[o + 2 * w - 2] = plane[i + w - 1] * 3 + plane[i + w - 2] + 1 >> 2; + out[o + 2 * w - 1] = plane[i + w - 1]; + } + } else if (hf === 1 && vf === 2 && fancy) { + for (let y = 0; y < h; y++) { + const i = y * stride; + const above = Math.max(0, y - 1) * stride; + const below = Math.min(h - 1, y + 1) * stride; + for (let x = 0; x < w; x++) { + const v = plane[i + x] * 3; + out[2 * y * W + x] = v + plane[above + x] + 1 >> 2; + out[(2 * y + 1) * W + x] = v + plane[below + x] + 2 >> 2; + } + } + } else if (hf === 2 && vf === 2 && fancy) { + for (let y = 0; y < h; y++) { + const i = y * stride; + + for (let pass = 0; pass < 2; pass++) { + const other = (pass === 0 ? Math.max(0, y - 1) : Math.min(h - 1, y + 1)) * stride; + const o = (2 * y + pass) * W; + + let thisSum = plane[i] * 3 + plane[other]; + let nextSum = plane[i + 1] * 3 + plane[other + 1]; + out[o] = thisSum * 4 + 8 >> 4; + out[o + 1] = thisSum * 3 + nextSum + 7 >> 4; + let lastSum = thisSum; + thisSum = nextSum; + + for (let x = 1; x < w - 1; x++) { + nextSum = plane[i + x + 1] * 3 + plane[other + x + 1]; + out[o + 2 * x] = thisSum * 3 + lastSum + 8 >> 4; + out[o + 2 * x + 1] = thisSum * 3 + nextSum + 7 >> 4; + lastSum = thisSum; + thisSum = nextSum; + } + + out[o + 2 * w - 2] = thisSum * 3 + lastSum + 8 >> 4; + out[o + 2 * w - 1] = thisSum * 4 + 7 >> 4; + } + } + } else { + // plain replication for every other sampling ratio + for (let y = 0; y < H; y++) { + const i = Math.min(h - 1, y / vf | 0) * stride; + for (let x = 0; x < W; x++) out[y * W + x] = plane[i + Math.min(w - 1, x / hf | 0)]; + } + } + + return { samples: out, stride: W }; +} + +// libjpeg's fixed point YCbCr -> RGB tables +const crR = new Int32Array(256); +const cbB = new Int32Array(256); +const crG = new Int32Array(256); +const cbG = new Int32Array(256); +for (let i = 0; i < 256; i++) { + const x = i - 128; + crR[i] = 91881 * x + 32768 >> 16; + cbB[i] = 116130 * x + 32768 >> 16; + crG[i] = -46802 * x; + cbG[i] = -22554 * x + 32768; +} + +function output(frame: Frame, adobe: { transform: number } | null, jfif: boolean, override: DecodeOptions['transform']): Uint8Array { + const { width, height, components } = frame; + const planes = components.map(component => upsample(component, frame)); + const pixels = new Uint8Array(width * height * 4); + const count = components.length; + + let transform: Transform; + if (count === 1) transform = 'grey'; + else if (override && (count === 4) === (override === 'cmyk')) transform = override; + else if (count === 3) { + if (adobe) transform = adobe.transform === 0 ? 'rgb' : 'ycc'; + else if (!jfif && components[0].id === 0x52 && components[1].id === 0x47 && components[2].id === 0x42) transform = 'rgb'; + else transform = 'ycc'; + } else if (count === 4) transform = adobe && adobe.transform === 2 ? 'ycck' : 'cmyk'; + else throw new Error(`JPEG: ${count} color components are not supported`); + + const [p0, p1, p2, p3] = planes; + + for (let y = 0, o = 0; y < height; y++) { + const r0 = y * p0.stride, r1 = p1 ? y * p1.stride : 0, r2 = p2 ? y * p2.stride : 0, r3 = p3 ? y * p3.stride : 0; + + for (let x = 0; x < width; x++, o += 4) { + pixels[o + 3] = 0xff; + + if (transform === 'grey') { + pixels[o] = pixels[o + 1] = pixels[o + 2] = p0.samples[r0 + x]; + continue; + } + + const a = p0.samples[r0 + x], b = p1.samples[r1 + x], c = p2.samples[r2 + x]; + + if (transform === 'rgb') { + pixels[o] = a; + pixels[o + 1] = b; + pixels[o + 2] = c; + continue; + } + + if (transform === 'ycc') { + pixels[o] = clamp8(a + crR[c]); + pixels[o + 1] = clamp8(a + (cbG[b] + crG[c] >> 16)); + pixels[o + 2] = clamp8(a + cbB[b]); + continue; + } + + let cyan = a, magenta = b, yellow = c, k = p3.samples[r3 + x]; + if (transform === 'ycck') { + cyan = 255 - clamp8(a + crR[c]); + magenta = 255 - clamp8(a + (cbG[b] + crG[c] >> 16)); + yellow = 255 - clamp8(a + cbB[b]); + } + + // CMYK data written by Adobe applications is stored inverted + if (adobe) { + cyan = 255 - cyan; + magenta = 255 - magenta; + yellow = 255 - yellow; + k = 255 - k; + } + + pixels[o] = cmykToRGB(cyan, k); + pixels[o + 1] = cmykToRGB(magenta, k); + pixels[o + 2] = cmykToRGB(yellow, k); + } + } + + return pixels; +} + +function cmykToRGB(c: number, k: number): number { + const t = (255 - c) * k + 128; + return clamp8(255 - (c + ((t >> 8) + t >> 8))); +} + +// --------------------------------------------------------------------------- +// encoder +// --------------------------------------------------------------------------- + +// ITU T.81 Annex K quantization tables, in natural order +const LUMA_QUANTIZATION = [ + 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99 +]; + +const CHROMA_QUANTIZATION = [ + 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99 +]; + +const AAN_SCALE = [1, 1.387039845, 1.306562965, 1.175875602, 1, 0.785694958, 0.5411961, 0.275899379]; + +/** + * Encodes RGBA pixel data into a baseline JPEG (alpha is ignored) + * Uses 4:2:0 chroma subsampling up to quality 90 and 4:4:4 above, with optimized huffman tables + * @param {Uint8Array} pixels + * @param {number} width + * @param {number} height + * @param {number} [quality=90] 1-100 + * @return {Uint8Array} + */ +export function encode(pixels: Uint8Array, width: number, height: number, quality = 90): Uint8Array { + if (!width || !height || width > 0xffff || height > 0xffff) throw new RangeError('JPEG: invalid dimensions'); + + quality = Math.min(100, Math.max(1, Math.round(quality))); + const scale = quality < 50 ? 5000 / quality : 200 - quality * 2; + const scaleTable = (base: number[]): number[] => base.map(v => Math.min(255, Math.max(1, Math.floor((v * scale + 50) / 100)))); + const tables = [scaleTable(LUMA_QUANTIZATION), scaleTable(CHROMA_QUANTIZATION)]; + + // reciprocal divisors for the float AAN FDCT + const divisors = tables.map(table => { + const d = new Float64Array(64); + for (let i = 0; i < 64; i++) d[i] = 1 / (table[i] * AAN_SCALE[i >> 3] * AAN_SCALE[i & 7] * 8); + return d; + }); + + const subsample = quality <= 90; + const hmax = subsample ? 2 : 1; + const mcuSize = 8 * hmax; + const mcusPerLine = Math.ceil(width / mcuSize); + const mcusPerColumn = Math.ceil(height / mcuSize); + + // convert to YCbCr planes padded to whole MCUs by edge replication + const W = mcusPerLine * mcuSize, H = mcusPerColumn * mcuSize; + const Y = new Float32Array(W * H), Cb = new Float32Array(W * H), Cr = new Float32Array(W * H); + + for (let y = 0; y < height; y++) { + let i = y * width * 4, o = y * W; + + for (let x = 0; x < width; x++, i += 4, o++) { + const r = pixels[i], g = pixels[i + 1], b = pixels[i + 2]; + Y[o] = 0.299 * r + 0.587 * g + 0.114 * b - 128; + Cb[o] = -0.168735892 * r - 0.331264108 * g + 0.5 * b; + Cr[o] = 0.5 * r - 0.418687589 * g - 0.081312411 * b; + } + + Y.fill(Y[o - 1], o, (y + 1) * W); + Cb.fill(Cb[o - 1], o, (y + 1) * W); + Cr.fill(Cr[o - 1], o, (y + 1) * W); + } + + for (let y = height; y < H; y++) { + Y.copyWithin(y * W, (height - 1) * W, height * W); + Cb.copyWithin(y * W, (height - 1) * W, height * W); + Cr.copyWithin(y * W, (height - 1) * W, height * W); + } + + // 2x2 box filter for subsampled chroma + const downsample = (plane: Float32Array): Float32Array => { + const out = new Float32Array(W * H / 4); + for (let y = 0, o = 0; y < H; y += 2) + for (let x = 0; x < W; x += 2, o++) { + const i = y * W + x; + out[o] = (plane[i] + plane[i + 1] + plane[i + W] + plane[i + W + 1]) * 0.25; + } + return out; + }; + + const chromaStride = subsample ? W / 2 : W; + const components = [ + { plane: Y, stride: W, h: hmax, v: hmax, table: 0 }, + { plane: subsample ? downsample(Cb) : Cb, stride: chromaStride, h: 1, v: 1, table: 1 }, + { plane: subsample ? downsample(Cr) : Cr, stride: chromaStride, h: 1, v: 1, table: 1 } + ]; + + // quantized blocks in MCU order (Y blocks, then Cb, then Cr) + const blocksPerMcu = hmax * hmax + 2; + const count = mcusPerLine * mcusPerColumn * blocksPerMcu; + const coefficients = new Int16Array(count * 64); + const owners = new Uint8Array(count); + const block = new Float64Array(64); + let n = 0; + + for (let my = 0; my < mcusPerColumn; my++) + for (let mx = 0; mx < mcusPerLine; mx++) + for (let c = 0; c < 3; c++) { + const { plane, stride, h: ch, v: cv, table } = components[c]; + const d = divisors[table]; + + for (let v = 0; v < cv; v++) + for (let h = 0; h < ch; h++, n++) { + const x0 = (mx * ch + h) * 8, y0 = (my * cv + v) * 8; + + for (let y = 0; y < 8; y++) { + const row = (y0 + y) * stride + x0; + for (let x = 0; x < 8; x++) block[y * 8 + x] = plane[row + x]; + } + + fdct(block); + const o = n * 64; + // round half up like libjpeg, without Math.round + for (let i = 0; i < 64; i++) coefficients[o + i] = (block[i] * d[i] + 16384.5 | 0) - 16384; + owners[n] = c; + } + } + + const blocks = { coefficients, owners, count }; + + // pass 1: gather symbol statistics, pass 2: write with optimal tables + const frequencies = [new Int32Array(257), new Int32Array(257), new Int32Array(257), new Int32Array(257)]; + encodeBlocks(blocks, (table, symbol) => frequencies[table][symbol]++, () => { }); + + const huffman = frequencies.map(buildOptimalTable); + const writer = new BitWriter(width * height); + + writer.bytes(0xff, 0xd8); + // JFIF APP0 + writer.bytes(0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00); + + // DQT + writer.bytes(0xff, 0xdb, 0x00, 0x84); + for (let t = 0; t < 2; t++) { + writer.bytes(t); + for (let k = 0; k < 64; k++) writer.bytes(tables[t][ZIGZAG[k]]); + } + + // SOF0 + writer.bytes(0xff, 0xc0, 0x00, 0x11, 0x08, height >> 8, height & 0xff, width >> 8, width & 0xff, 0x03); + writer.bytes(1, hmax << 4 | hmax, 0, 2, 0x11, 1, 3, 0x11, 1); + + // DHT: luma DC, luma AC, chroma DC, chroma AC + for (const [index, id] of [[0, 0x00], [1, 0x10], [2, 0x01], [3, 0x11]] as Array<[number, number]>) { + const { bits, values } = huffman[index]; + const length = 19 + values.length; + writer.bytes(0xff, 0xc4, length >> 8, length & 0xff, id); + for (let i = 1; i <= 16; i++) writer.bytes(bits[i]); + for (const value of values) writer.bytes(value); + } + + // SOS + writer.bytes(0xff, 0xda, 0x00, 0x0c, 0x03, 1, 0x00, 2, 0x11, 3, 0x11, 0x00, 0x3f, 0x00); + + encodeBlocks(blocks, + (table, symbol) => writer.write(huffman[table].codes[symbol], huffman[table].sizes[symbol]), + (value, size) => writer.write(value, size)); + + writer.flush(); + writer.bytes(0xff, 0xd9); + return writer.finish(); +} + +// tables: 0 luma DC, 1 luma AC, 2 chroma DC, 3 chroma AC +interface Blocks { + coefficients: Int16Array; + owners: Uint8Array; + count: number; +} + +function encodeBlocks({ coefficients, owners, count }: Blocks, symbol: (table: number, symbol: number) => void, bits: (value: number, size: number) => void): void { + const pred = [0, 0, 0]; + + for (let n = 0; n < count; n++) { + const component = owners[n]; + const o = n * 64; + const dcTable = component === 0 ? 0 : 2; + const acTable = dcTable + 1; + + const diff = coefficients[o] - pred[component]; + pred[component] = coefficients[o]; + const size = magnitude(diff); + symbol(dcTable, size); + if (size) bits(diff < 0 ? diff - 1 & (1 << size) - 1 : diff, size); + + let run = 0; + for (let k = 1; k < 64; k++) { + const value = coefficients[o + ZIGZAG[k]]; + + if (value === 0) { + run++; + continue; + } + + while (run > 15) { + symbol(acTable, 0xf0); + run -= 16; + } + + const s = magnitude(value); + symbol(acTable, run << 4 | s); + bits(value < 0 ? value - 1 & (1 << s) - 1 : value, s); + run = 0; + } + + if (run) symbol(acTable, 0x00); + } +} + +function magnitude(value: number): number { + value = Math.abs(value); + let size = 0; + while (value) { + size++; + value >>= 1; + } + return size; +} + +// libjpeg's jpeg_gen_optimal_table (ITU T.81 Annex K.2) +interface OptimalTable { + bits: Int32Array; + values: number[]; + codes: Int32Array; + sizes: Int32Array; +} + +function buildOptimalTable(frequencies: Int32Array): OptimalTable { + const freq = Int32Array.from(frequencies); + const codesize = new Int32Array(257); + const others = new Int32Array(257).fill(-1); + freq[256] = 1; // reserved, guarantees no code consists of all ones + + for (; ;) { + let c1 = -1, c2 = -1, v1 = Infinity, v2 = Infinity; + for (let i = 0; i <= 256; i++) + if (freq[i] && freq[i] <= v1) { + v1 = freq[i]; + c1 = i; + } + for (let i = 0; i <= 256; i++) + if (freq[i] && freq[i] <= v2 && i !== c1) { + v2 = freq[i]; + c2 = i; + } + if (c2 < 0) break; + + freq[c1] += freq[c2]; + freq[c2] = 0; + + codesize[c1]++; + while (others[c1] >= 0) { + c1 = others[c1]; + codesize[c1]++; + } + others[c1] = c2; + + codesize[c2]++; + while (others[c2] >= 0) { + c2 = others[c2]; + codesize[c2]++; + } + } + + const bits = new Int32Array(33); + for (let i = 0; i <= 256; i++) if (codesize[i]) bits[codesize[i]]++; + + // limit code lengths to 16 bits + for (let i = 32; i > 16; i--) { + while (bits[i] > 0) { + let j = i - 2; + while (bits[j] === 0) j--; + bits[i] -= 2; + bits[i - 1]++; + bits[j + 1] += 2; + bits[j]--; + } + } + + // remove the reserved code point + let i = 16; + while (bits[i] === 0) i--; + bits[i]--; + + const values = []; + for (let length = 1; length <= 32; length++) + for (let symbol = 0; symbol < 256; symbol++) + if (codesize[symbol] === length) values.push(symbol); + + // canonical codes + const codes = new Int32Array(256); + const sizes = new Int32Array(256); + let code = 0, k = 0; + for (let length = 1; length <= 16; length++) { + for (let n = 0; n < bits[length]; n++, k++) { + codes[values[k]] = code++; + sizes[values[k]] = length; + } + code <<= 1; + } + + return { bits: bits.subarray(0, 17), values, codes, sizes }; +} + +// libjpeg's jpeg_fdct_float (AAN), output scaled by AAN_SCALE[row] * AAN_SCALE[col] * 8 +function fdct(d: Float64Array): void { + for (let p = 0; p < 64; p += 8) fdctPass(d, p, 1); + for (let p = 0; p < 8; p++) fdctPass(d, p, 8); +} + +function fdctPass(d: Float64Array, o: number, s: number): void { + const tmp0 = d[o] + d[o + 7 * s], tmp7 = d[o] - d[o + 7 * s]; + const tmp1 = d[o + s] + d[o + 6 * s], tmp6 = d[o + s] - d[o + 6 * s]; + const tmp2 = d[o + 2 * s] + d[o + 5 * s], tmp5 = d[o + 2 * s] - d[o + 5 * s]; + const tmp3 = d[o + 3 * s] + d[o + 4 * s], tmp4 = d[o + 3 * s] - d[o + 4 * s]; + + let tmp10 = tmp0 + tmp3, tmp13 = tmp0 - tmp3; + let tmp11 = tmp1 + tmp2, tmp12 = tmp1 - tmp2; + + d[o] = tmp10 + tmp11; + d[o + 4 * s] = tmp10 - tmp11; + + const z1 = (tmp12 + tmp13) * 0.707106781; + d[o + 2 * s] = tmp13 + z1; + d[o + 6 * s] = tmp13 - z1; + + tmp10 = tmp4 + tmp5; + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + const z5 = (tmp10 - tmp12) * 0.382683433; + const z2 = 0.5411961 * tmp10 + z5; + const z4 = 1.306562965 * tmp12 + z5; + const z3 = tmp11 * 0.707106781; + + const z11 = tmp7 + z3, z13 = tmp7 - z3; + d[o + 5 * s] = z13 + z2; + d[o + 3 * s] = z13 - z2; + d[o + s] = z11 + z4; + d[o + 7 * s] = z11 - z4; +} + +class BitWriter { + buffer: Uint8Array; + length: number; + bits: number; + count: number; + + constructor(capacity: number) { + this.buffer = new Uint8Array(Math.max(1024, capacity)); + this.length = 0; + this.bits = 0; + this.count = 0; + } + + reserve(size: number): void { + if (this.length + size <= this.buffer.length) return; + const buffer = new Uint8Array(Math.max(this.buffer.length * 2, this.length + size)); + buffer.set(this.buffer.subarray(0, this.length)); + this.buffer = buffer; + } + + bytes(...values: number[]): void { + this.reserve(values.length); + for (const value of values) this.buffer[this.length++] = value; + } + + // writes `size` bits MSB first, stuffing a zero after every 0xff byte + write(value: number, size: number): void { + this.bits = this.bits << size | value & (1 << size) - 1; + this.count += size; + + while (this.count >= 8) { + this.count -= 8; + const byte = this.bits >>> this.count & 0xff; + this.reserve(2); + this.buffer[this.length++] = byte; + if (byte === 0xff) this.buffer[this.length++] = 0x00; + } + + this.bits &= (1 << this.count) - 1; + } + + // pads the last byte with ones + flush(): void { + if (this.count) this.write((1 << 8 - this.count) - 1, 8 - this.count); + } + + finish(): Uint8Array { + return this.buffer.slice(0, this.length); + } +} + diff --git a/src/codecs/png.ts b/src/codecs/png.ts new file mode 100644 index 0000000..2e7643a --- /dev/null +++ b/src/codecs/png.ts @@ -0,0 +1,533 @@ +import { crc32 } from './crc.ts'; +import { concat } from '../util/mem.ts'; +import { encode as utf8, decode as utf8Decode } from '../util/utf8.ts'; +import { orientation as exifOrientation, xmpOrientation } from './exif.ts'; +import { compressAsync, decompressAsync } from './zlib.ts'; + +const HEAD = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10); +const IHDR = 0x49484452; +const PLTE = 0x504c5445; +const tRNS = 0x74524e53; +const IDAT = 0x49444154; +const IEND = 0x49454e44; +const eXIf = 0x65584966; +const tEXt = 0x74455874; +const zTXt = 0x7a545874; +const iTXt = 0x69545874; + +const COLOR_TYPE_CHANNELS: Record = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }; +const CHANNELS_TO_COLOR_TYPE: Record = { 1: 0, 2: 4, 3: 2, 4: 6 }; +const VALID_DEPTHS: Record = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] }; + +export interface EncodeOptions { + /** tEXt chunks, empty values are skipped */ + text?: Record; + width: number; + height: number; + channels: number; + depth?: number; + level?: number; + /** packed RGB palette, makes the data palette indices (1 channel) */ + palette?: Uint8Array; + /** alpha per palette entry */ + transparency?: Uint8Array; + /** whether to paeth filter the rows (pays off for truecolor and greyscale images) */ + filter?: boolean; +} + +export interface Decoded { + width: number; + height: number; + /** 8-bit RGBA */ + pixels: Uint8Array; + /** Exif orientation (1-8) */ + orientation: number; +} + +// where the pixels of an (interlacing) pass go: output offset of the pass' row y, and the step between its pixels +interface Placement { + row: (y: number) => number; + step: number; +} + +type Converter = (pixels: Uint8Array, rowBytes: number, w: number, h: number, out: Uint8Array, place: Placement) => void; + +// [x start, y start, x step, y step] per adam7 pass +const ADAM7 = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]]; + + +/** + * Encodes raw pixel data into a PNG + * @param data Raw pixel data, rows packed MSB first for depths below 8 + */ +export async function encode(data: Uint8Array, { text, width, height, channels, depth = 8, level = 0, palette, transparency, filter: filterRows = false }: EncodeOptions): Promise { + const rowBytes = Math.ceil(width * channels * depth / 8); + let filtered: Uint8Array; + if (filterRows) filtered = filter(data, rowBytes, height, Math.max(1, channels * depth / 8)); + else { + // prefix every row with filter type 0 (none) + filtered = new Uint8Array(height * (rowBytes + 1)); + for (let y = 0; y < height; y++) filtered.set(data.subarray(y * rowBytes, (y + 1) * rowBytes), y * (rowBytes + 1) + 1); + } + + const header = new Uint8Array(13); + const view = new DataView(header.buffer); + view.setUint32(0, width); + view.setUint32(4, height); + header[8] = depth; + header[9] = palette ? 3 : CHANNELS_TO_COLOR_TYPE[channels]; + + const chunks = [chunk(IHDR, header)]; + if (palette) chunks.push(chunk(PLTE, palette)); + if (transparency) chunks.push(chunk(tRNS, transparency)); + chunks.push(chunk(IDAT, await compressAsync(filtered, level))); + + for (const key in text) { + const value = text[key]; + if (!value) continue; + chunks.push(chunk(0x74455874, concat([utf8(key), [0], utf8(value)]))); + } + + chunks.push(chunk(IEND, new Uint8Array(0))); + return concat([HEAD, ...chunks]); +} + +/** + * Filters every row with the paeth filter. Picking a filter per row (libpng's heuristic) costs twice as much and + * compresses within 0.1% of it for photos, graphics and gradients alike + */ +function filter(data: Uint8Array, rowBytes: number, height: number, bpp: number): Uint8Array { + const out = new Uint8Array(height * (rowBytes + 1)); + + for (let y = 0; y < height; y++) { + const row = y * rowBytes, o = y * (rowBytes + 1) + 1; + out[o - 1] = 4; + + // the first row has no row above, the first pixel of every row no left neighbour + if (y === 0) { + for (let i = 0; i < rowBytes; i++) out[o + i] = data[row + i] - (i >= bpp ? data[row + i - bpp] : 0); + continue; + } + + for (let i = 0; i < bpp; i++) out[o + i] = data[row + i] - data[row + i - rowBytes]; + for (let i = bpp; i < rowBytes; i++) { + const a = data[row + i - bpp], b = data[row + i - rowBytes], c = data[row + i - rowBytes - bpp]; + const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c); + out[o + i] = data[row + i] - (pa <= pb && pa <= pc ? a : pb <= pc ? b : c); + } + } + + return out; +} + +function chunk(type: number, body: Uint8Array): Uint8Array { + const out = new Uint8Array(body.length + 12); + const view = new DataView(out.buffer); + view.setUint32(0, body.length); + view.setUint32(4, type); + out.set(body, 8); + view.setUint32(body.length + 8, crc32(out.subarray(4, body.length + 8))); + return out; +} + +/** + * Encodes 8-bit RGBA pixels into a PNG with the smallest lossless color type: + * a palette for up to 256 colors, otherwise greyscale or RGB, with alpha only if a pixel isn't opaque. + * Rows of non-palette images are paeth filtered (unless the data isn't compressed), palette indices aren't + * filtered (like libpng recommends) + */ +export async function encodeRGBA(rgba: Uint8Array, width: number, height: number, level: number, text?: EncodeOptions['text']): Promise { + const count = width * height; + if (rgba.byteOffset % 4) rgba = rgba.slice(0, count * 4); + const pixels = new Uint32Array(rgba.buffer, rgba.byteOffset, count); + let opaque = true, grey = true; + + // collect up to 257 distinct colors, runs of equal pixels are skipped cheaply + let colors: Map | null = new Map(); + let previous = -1; + for (let i = 0; i < count; i++) { + const value = pixels[i]; + if (value === previous) continue; + previous = value; + + const o = i * 4; + if (rgba[o + 3] !== 255) opaque = false; + if (rgba[o] !== rgba[o + 1] || rgba[o] !== rgba[o + 2]) grey = false; + if (colors && !colors.has(value)) { + if (colors.size === 256) colors = null; + else colors.set(value, colors.size); + } + } + + const options = { width, height, level, text, filter: level > 0 }; + + if (colors) { + // opaque entries are moved to the end, so tRNS can be cut short + const entries = [...colors.keys()].map(value => new Uint8Array(new Uint32Array([value]).buffer)); + entries.sort((a, b) => a[3] - b[3]); + const index = new Map(); + entries.forEach((bytes, i) => index.set(new Uint32Array(bytes.buffer)[0], i)); + + const palette = new Uint8Array(entries.length * 3); + entries.forEach((bytes, i) => palette.set(bytes.subarray(0, 3), i * 3)); + const translucent = entries.filter(bytes => bytes[3] !== 255).length; + const transparency = translucent ? Uint8Array.from(entries.slice(0, translucent), bytes => bytes[3]) : undefined; + + const depth = entries.length <= 2 ? 1 : entries.length <= 4 ? 2 : entries.length <= 16 ? 4 : 8; + const rowBytes = Math.ceil(width * depth / 8); + const perByte = 8 / depth; + const data = new Uint8Array(rowBytes * height); + + let last = -1, lastIndex = 0; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const value = pixels[y * width + x]; + if (value !== last) { + last = value; + lastIndex = index.get(value) as number; + } + data[y * rowBytes + (x / perByte | 0)] |= lastIndex << (8 - depth - (x % perByte) * depth); + } + } + + return encode(data, { ...options, channels: 1, depth, palette, transparency, filter: false }); + } + + const channels = (grey ? 1 : 3) + (opaque ? 0 : 1); + if (channels === 4) return encode(rgba, { ...options, channels }); + + const data = new Uint8Array(count * channels); + for (let i = 0, o = 0; i < count; i++) { + const s = i * 4; + if (grey) data[o++] = rgba[s]; + else { + data[o++] = rgba[s]; + data[o++] = rgba[s + 1]; + data[o++] = rgba[s + 2]; + } + if (!opaque) data[o++] = rgba[s + 3]; + } + + return encode(data, { ...options, channels }); +} + +/** + * Decodes a PNG into 8-bit RGBA pixel data + * Supports all color types, bit depths, tRNS transparency and adam7 interlacing + * The orientation comes from the eXIf chunk, or, if there is none, from the Exif text ImageMagick wrote before eXIf + * existed ("Raw profile type exif") or from the XMP packet + * @param {Uint8Array} array The PNG data + * @return {{width: number, height: number, pixels: Uint8Array, orientation: number}} + */ +export async function decode(array: Uint8Array): Promise { + const view = new DataView(array.buffer, array.byteOffset, array.byteLength); + + for (let i = 0; i < HEAD.length; i++) + if (array[i] !== HEAD[i]) throw new Error('PNG: invalid signature'); + + let header: { width: number, height: number, depth: number, colorType: number, interlace: number } | null = null; + let palette: Uint8Array | null = null; + let transparency: Uint8Array | null = null; + const idat: Uint8Array[] = []; + let exif: Uint8Array | null = null; + const text: Array<{ type: number, chunk: Uint8Array }> = []; + + let offset = 8; + while (offset + 8 <= array.length) { + const length = view.getUint32(offset); + const type = view.getUint32(offset + 4); + const start = offset + 8; + const end = start + length; + + if (end > array.length) { + // tolerate truncated trailing chunks as long as we already have image data + if (type === IDAT) idat.push(array.subarray(start)); + break; + } + + const chunk = array.subarray(start, end); + + if (end + 4 <= array.length && crc32(array.subarray(offset + 4, end)) !== view.getUint32(end)) + throw new Error('PNG: chunk checksum mismatch'); + + if (type === IHDR) { + if (length < 13) throw new Error('PNG: invalid IHDR'); + header = { + width: view.getUint32(start), + height: view.getUint32(start + 4), + depth: chunk[8], + colorType: chunk[9], + interlace: chunk[12] + }; + } else if (type === PLTE) { + if (palette) throw new Error('PNG: PLTE can only occur once in an image'); + palette = chunk; + } else if (type === tRNS) { + if (transparency) throw new Error('PNG: tRNS can only occur once in an image'); + transparency = chunk; + } else if (type === IDAT) idat.push(chunk); + else if (type === eXIf) exif ??= chunk; + else if (type === tEXt || type === zTXt || type === iTXt) text.push({ type, chunk }); + else if (type === IEND) break; + + offset = end + 4; + } + + if (!header) throw new Error('PNG: missing IHDR'); + if (!idat.length) throw new Error('PNG: missing IDAT'); + + const { width, height, depth, colorType, interlace } = header; + const channels = COLOR_TYPE_CHANNELS[colorType]; + if (!channels) throw new Error(`PNG: invalid color type ${colorType}`); + if (!VALID_DEPTHS[colorType].includes(depth)) throw new Error(`PNG: invalid bit depth ${depth} for color type ${colorType}`); + if (width === 0 || height === 0) throw new Error('PNG: invalid dimensions'); + if (colorType === 3 && !palette) throw new Error('PNG: indexed color image has no PLTE'); + + const bitsPerPixel = channels * depth; + const bpp = Math.max(1, bitsPerPixel >> 3); + const passes = interlace ? ADAM7.map(([xs, ys, dx, dy]) => [xs, ys, dx, dy, Math.ceil((width - xs) / dx), Math.ceil((height - ys) / dy)]) : [[0, 0, 1, 1, width, height]]; + + let expected = 0; + for (const pass of passes) { + const [, , , , pw, ph] = pass; + if (pw > 0 && ph > 0) expected += ph * (1 + Math.ceil(pw * bitsPerPixel / 8)); + } + + const data = await decompressAsync(idat.length === 1 ? idat[0] : concat(idat), expected); + if (data.length < expected) throw new Error('PNG: image data is truncated'); + + const rgba = new Uint8Array(width * height * 4); + const toRGBA = createConverter(colorType, depth, palette, transparency); + + let dataOffset = 0; + for (const [xs, ys, dx, dy, pw, ph] of passes) { + if (pw <= 0 || ph <= 0) continue; + + const rowBytes = Math.ceil(pw * bitsPerPixel / 8); + const size = ph * rowBytes; + const pixels = unfilter(data, dataOffset, ph, rowBytes, bpp); + dataOffset += size + ph; + + toRGBA(pixels, rowBytes, pw, ph, rgba, { row: y => ((ys + y * dy) * width + xs) * 4, step: dx * 4 }); + } + + return { width, height, pixels: rgba, orientation: exif ? exifOrientation(exif) : await textOrientation(text) }; +} + +const RAW_EXIF = /^Raw profile type (exif|APP1)$/; +const XMP = 'XML:com.adobe.xmp'; + +/** + * Finds the orientation in Exif or XMP text chunks + */ +async function textOrientation(chunks: Array<{ type: number, chunk: Uint8Array }>): Promise { + for (const { type, chunk } of chunks) { + const separator = chunk.indexOf(0); + if (separator < 1) continue; + const keyword = utf8Decode(chunk.subarray(0, separator)); + if (!RAW_EXIF.test(keyword) && keyword !== XMP) continue; + + let body: Uint8Array; + try { + if (type === tEXt) body = chunk.subarray(separator + 1); + else if (type === zTXt) body = await decompressAsync(chunk.subarray(separator + 2), 1 << 20); + else { + // compression flag and method, then language and translated keyword (both null terminated) + const language = chunk.indexOf(0, separator + 3); + const translated = language < 0 ? -1 : chunk.indexOf(0, language + 1); + if (translated < 0) continue; + body = chunk[separator + 1] ? await decompressAsync(chunk.subarray(translated + 1), 1 << 20) : chunk.subarray(translated + 1); + } + } catch { + continue; + } + + const value = utf8Decode(body); + const orientation = keyword === XMP ? xmpOrientation(value) : exifOrientation(hexProfile(value)); + if (orientation !== 1) return orientation; + } + + return 1; +} + +/** + * Parses ImageMagick's hex encoded profiles: "\n\n\n" + */ +function hexProfile(text: string): Uint8Array { + const match = /^\s*\S+\s+(\d+)\s+([\da-fA-F\s]+)/.exec(text); + if (!match) return new Uint8Array(0); + const digits = match[2].replace(/\s+/g, ''); + const out = new Uint8Array(Math.min(+match[1], digits.length >> 1)); + for (let i = 0; i < out.length; i++) out[i] = parseInt(digits.substr(i * 2, 2), 16); + return out; +} + +function unfilter(data: Uint8Array, offset: number, height: number, rowBytes: number, bpp: number): Uint8Array { + const out = new Uint8Array(height * rowBytes); + const first = Math.min(bpp, rowBytes); + + // the first row and the first pixel of every row have zero neighbours, those are handled by separate loops + for (let y = 0; y < height; y++) { + const filter = data[offset++]; + const row = y * rowBytes; + const src = offset - row; // data index = src + out index + + if (filter === 0 || (filter === 2 && y === 0)) out.set(data.subarray(offset, offset + rowBytes), row); + else if (filter === 1) { + out.set(data.subarray(offset, offset + first), row); + for (let i = row + bpp; i < row + rowBytes; i++) out[i] = data[src + i] + out[i - bpp]; + } else if (filter === 2) { + for (let i = row; i < row + rowBytes; i++) out[i] = data[src + i] + out[i - rowBytes]; + } else if (filter === 3) { + if (y === 0) { + out.set(data.subarray(offset, offset + first), row); + for (let i = row + bpp; i < row + rowBytes; i++) out[i] = data[src + i] + (out[i - bpp] >> 1); + } else { + for (let i = row; i < row + first; i++) out[i] = data[src + i] + (out[i - rowBytes] >> 1); + for (let i = row + bpp; i < row + rowBytes; i++) out[i] = data[src + i] + (out[i - bpp] + out[i - rowBytes] >> 1); + } + } else if (filter === 4) { + if (y === 0) { + // paeth with no row above picks the left neighbour + out.set(data.subarray(offset, offset + first), row); + for (let i = row + bpp; i < row + rowBytes; i++) out[i] = data[src + i] + out[i - bpp]; + } else { + // and with no left neighbour the one above + for (let i = row; i < row + first; i++) out[i] = data[src + i] + out[i - rowBytes]; + for (let i = row + bpp; i < row + rowBytes; i++) { + const a = out[i - bpp], b = out[i - rowBytes], c = out[i - rowBytes - bpp]; + const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c); + out[i] = data[src + i] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c); + } + } + } else throw new Error(`PNG: invalid filter type ${filter}`); + + offset += rowBytes; + } + + return out; +} + +function createConverter(colorType: number, depth: number, palette: Uint8Array | null, transparency: Uint8Array | null): Converter { + const maxValue = (1 << depth) - 1; + + // reads the n-th sample of a row; samples of depth < 8 are packed MSB first + const sample: (row: Uint8Array, n: number) => number = depth === 8 ? (row, n) => row[n] + : depth === 16 ? (row, n) => row[n * 2] << 8 | row[n * 2 + 1] + : (row, n) => { + const bit = n * depth; + return (row[bit >> 3] >> (8 - depth - (bit & 7))) & maxValue; + }; + + // scales a raw sample to 8 bits + const scale: (v: number) => number = depth === 8 ? v => v : depth === 16 ? v => v >> 8 : v => Math.round(v * 255 / maxValue); + + if (colorType === 3) { + const pal = palette as Uint8Array; + const entries = pal.length / 3 | 0; + const lut = new Uint8Array(256 * 4); + + for (let i = 0; i < 256; i++) { + if (i < entries) { + lut[i * 4] = pal[i * 3]; + lut[i * 4 + 1] = pal[i * 3 + 1]; + lut[i * 4 + 2] = pal[i * 3 + 2]; + } + + lut[i * 4 + 3] = transparency && i < transparency.length ? transparency[i] : 0xff; + } + + return (pixels, rowBytes, w, h, out, place) => { + for (let y = 0; y < h; y++) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes); + + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) { + const i = sample(row, x) * 4; + out[o] = lut[i]; + out[o + 1] = lut[i + 1]; + out[o + 2] = lut[i + 2]; + out[o + 3] = lut[i + 3]; + } + } + }; + } + + if (colorType === 0) { + const key = transparency && transparency.length >= 2 ? (transparency[0] << 8 | transparency[1]) & maxValue : -1; + + return (pixels, rowBytes, w, h, out, place) => { + for (let y = 0; y < h; y++) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes); + + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) { + const raw = sample(row, x); + const v = scale(raw); + out[o] = out[o + 1] = out[o + 2] = v; + out[o + 3] = raw === key ? 0 : 0xff; + } + } + }; + } + + if (colorType === 2) { + const key = transparency && transparency.length >= 6 + ? [transparency[0] << 8 | transparency[1], transparency[2] << 8 | transparency[3], transparency[4] << 8 | transparency[5]].map(v => v & maxValue) + : null; + + return (pixels, rowBytes, w, h, out, place) => { + for (let y = 0; y < h; y++) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes); + + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) { + const r = sample(row, x * 3); + const g = sample(row, x * 3 + 1); + const b = sample(row, x * 3 + 2); + out[o] = scale(r); + out[o + 1] = scale(g); + out[o + 2] = scale(b); + out[o + 3] = key && r === key[0] && g === key[1] && b === key[2] ? 0 : 0xff; + } + } + }; + } + + if (colorType === 4) { + return (pixels, rowBytes, w, h, out, place) => { + for (let y = 0; y < h; y++) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes); + + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) { + const v = scale(sample(row, x * 2)); + out[o] = out[o + 1] = out[o + 2] = v; + out[o + 3] = scale(sample(row, x * 2 + 1)); + } + } + }; + } + + // colorType === 6 + if (depth === 8) { + return (pixels, rowBytes, w, h, out, place) => { + // non-interlaced 8-bit RGBA maps 1:1 onto the output + if (place.step === 4 && w * 4 === rowBytes && place.row(0) === 0 && place.row(1) === rowBytes) return out.set(pixels); + + for (let y = 0; y < h; y++) + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) + out.set(pixels.subarray(y * rowBytes + x * 4, y * rowBytes + x * 4 + 4), o); + }; + } + + return (pixels, rowBytes, w, h, out, place) => { + for (let y = 0; y < h; y++) { + const row = pixels.subarray(y * rowBytes, (y + 1) * rowBytes); + + for (let x = 0, o = place.row(y); x < w; x++, o += place.step) { + out[o] = scale(sample(row, x * 4)); + out[o + 1] = scale(sample(row, x * 4 + 1)); + out[o + 2] = scale(sample(row, x * 4 + 2)); + out[o + 3] = scale(sample(row, x * 4 + 3)); + } + } + }; +} + diff --git a/src/codecs/quantize.ts b/src/codecs/quantize.ts new file mode 100644 index 0000000..39ceed5 --- /dev/null +++ b/src/codecs/quantize.ts @@ -0,0 +1,398 @@ + +// perceptual channel weights for squared color distances +const WR = 3, WG = 4, WB = 2; + +/** + * Reduces RGBA pixel data to an indexed palette + * Pixels with alpha < 128 are mapped to a dedicated transparent index + * @param {Uint8Array} pixels RGBA pixel data + * @param {number} width Image width in pixels (needed for dithering) + * @param {{maxColors?: number, quality?: number}} [options] quality (1-100) scales the palette size exponentially (1 = 8 colors, 50 = 55, 90 and above = 256) + * @return {{palette: Uint8Array, indices: Uint8Array, transparentIndex: number}} palette is packed RGB, transparentIndex is -1 if unused + */ +export interface QuantizeOptions { + maxColors?: number; + quality?: number; +} + +export interface Quantized { + /** packed RGB */ + palette: Uint8Array; + indices: Uint8Array; + /** -1 if no pixel is transparent */ + transparentIndex: number; +} + +interface Clusters { + /** mean color per cluster, packed RGB */ + palette: Float64Array; + /** pixel count per cluster */ + weights: Float64Array; +} + +/** + * Finds the color of the largest cluster among the opaque pixels that pass the filter + * @return packed RGB, or null if no pixel qualifies + */ +export function dominantColor(pixels: Uint8Array, include: (r: number, g: number, b: number) => boolean, clusters = 8): [number, number, number] | null { + const { palette, weights } = medianCut(pixels, clusters, 4, include); + if (!weights.length) return null; + + let best = 0; + for (let i = 1; i < weights.length; i++) if (weights[i] > weights[best]) best = i; + return [Math.round(palette[best * 3]), Math.round(palette[best * 3 + 1]), Math.round(palette[best * 3 + 2])]; +} + +interface Points { + /** mean color per channel */ + c: [Float64Array, Float64Array, Float64Array]; + /** pixel count */ + w: Float64Array; +} + +interface Box { + /** indices into the points */ + members: Int32Array; + weight: number; + mean: number[]; + axis: number; + error: number; +} + +export function quantize(pixels: Uint8Array, width: number, { maxColors = 256, quality = 95 }: QuantizeOptions = {}): Quantized { + const length = pixels.length >> 2; + let hasTransparency = false; + for (let i = 3; i < pixels.length; i += 4) + if (pixels[i] < 128) { + hasTransparency = true; + break; + } + + quality = Math.min(100, Math.max(1, quality)); + const limit = Math.min(maxColors, Math.round(8 * 32 ** (Math.min(quality, 90) / 90))); + const colors = hasTransparency ? limit - 1 : limit; + const exact = exactPalette(pixels, colors); + + let palette: Uint8Array, indices: Uint8Array; + if (exact) ({ palette, indices } = exact); + else { + palette = Uint8Array.from(medianCut(pixels, colors, Math.ceil(quality / 25)).palette, Math.round); + indices = ditherMap(pixels, width, palette, palette.length / 3); + } + + let transparentIndex = -1; + if (hasTransparency) { + transparentIndex = palette.length / 3; + const padded = new Uint8Array(palette.length + 3); + padded.set(palette); + palette = padded; + + for (let i = 0; i < length; i++) + if (pixels[i * 4 + 3] < 128) indices[i] = transparentIndex; + } + + return { palette, indices, transparentIndex }; +} + +// returns an exact palette if the opaque pixels use at most `limit` distinct colors +function exactPalette(pixels: Uint8Array, limit: number): { palette: Uint8Array, indices: Uint8Array } | null { + const lookup = new Map(); + const indices = new Uint8Array(pixels.length >> 2); + const palette: number[] = []; + + for (let i = 0, p = 0; i < pixels.length; i += 4, p++) { + if (pixels[i + 3] < 128) continue; + + const key = pixels[i] << 16 | pixels[i + 1] << 8 | pixels[i + 2]; + let index = lookup.get(key); + + if (index === undefined) { + if (lookup.size >= limit) return null; + index = lookup.size; + lookup.set(key, index); + palette.push(pixels[i], pixels[i + 1], pixels[i + 2]); + } + + indices[p] = index; + } + + // a palette needs at least one entry + if (!palette.length) palette.push(0, 0, 0); + return { palette: Uint8Array.from(palette), indices }; +} + +// weighted median cut on a 15-bit histogram, followed by k-means refinement +function medianCut(pixels: Uint8Array, colors: number, iterations: number, include?: (r: number, g: number, b: number) => boolean): Clusters { + const count = new Float64Array(32768); + const sumR = new Float64Array(32768); + const sumG = new Float64Array(32768); + const sumB = new Float64Array(32768); + + for (let i = 0; i < pixels.length; i += 4) { + if (pixels[i + 3] < 128) continue; + const r = pixels[i], g = pixels[i + 1], b = pixels[i + 2]; + if (include && !include(r, g, b)) continue; + const bin = (r >> 3) << 10 | (g >> 3) << 5 | b >> 3; + count[bin]++; + sumR[bin] += r; + sumG[bin] += g; + sumB[bin] += b; + } + + // collapse non-empty bins into weighted points (mean color + count), stored per channel + let n = 0; + for (let i = 0; i < 32768; i++) if (count[i]) n++; + + const points: Points = { c: [new Float64Array(n), new Float64Array(n), new Float64Array(n)], w: new Float64Array(n) }; + for (let i = 0, p = 0; i < 32768; i++) { + if (!count[i]) continue; + points.c[0][p] = sumR[i] / count[i]; + points.c[1][p] = sumG[i] / count[i]; + points.c[2][p] = sumB[i] / count[i]; + points.w[p++] = count[i]; + } + + if (!n) return { palette: new Float64Array(0), weights: new Float64Array(0) }; + const boxes = [makeBox(points, Int32Array.from({ length: n }, (_, i) => i))]; + const scratch = new Int32Array(n); + while (boxes.length < colors) { + let target = -1; + for (let i = 0; i < boxes.length; i++) + if (boxes[i].members.length > 1 && (target === -1 || boxes[i].error > boxes[target].error)) target = i; + if (target === -1) break; + + const { members, axis, weight } = boxes[target]; + sortBy(members, points.c[axis], scratch); + + // split at the weighted median, keeping at least one point on each side + const half = weight / 2; + let split = 1; + for (let acc = 0; split < members.length - 1; split++) { + acc += points.w[members[split - 1]]; + if (acc >= half) break; + } + + boxes.splice(target, 1, makeBox(points, members.subarray(0, split)), makeBox(points, members.subarray(split))); + } + + const palette = new Float64Array(boxes.length * 3); + boxes.forEach((box, i) => palette.set(box.mean, i * 3)); + + // k-means: reassign every point to its nearest palette color and move colors to their cluster means + const entries = boxes.length; + const [pr, pg, pb] = points.c, pw = points.w; + const weights = Float64Array.from(boxes, box => box.weight); + for (let iteration = 0; iteration < iterations; iteration++) { + const acc = new Float64Array(entries * 4); + const nearest = nearestSearch(palette, entries); + + for (let i = 0; i < n; i++) { + const index = nearest(pr[i], pg[i], pb[i]); + acc[index * 4] += pr[i] * pw[i]; + acc[index * 4 + 1] += pg[i] * pw[i]; + acc[index * 4 + 2] += pb[i] * pw[i]; + acc[index * 4 + 3] += pw[i]; + } + + for (let i = 0; i < entries; i++) { + const w = weights[i] = acc[i * 4 + 3]; + if (!w) continue; + palette[i * 3] = acc[i * 4] / w; + palette[i * 3 + 1] = acc[i * 4 + 1] / w; + palette[i * 3 + 2] = acc[i * 4 + 2] / w; + } + } + + return { palette, weights }; +} + +function makeBox({ c: [r, g, b], w }: Points, members: Int32Array): Box { + let weight = 0; + const sum = [0, 0, 0]; + const sq = [0, 0, 0]; + + for (let m = 0; m < members.length; m++) { + const i = members[m]; + weight += w[i]; + sum[0] += r[i] * w[i]; + sq[0] += r[i] * r[i] * w[i]; + sum[1] += g[i] * w[i]; + sq[1] += g[i] * g[i] * w[i]; + sum[2] += b[i] * w[i]; + sq[2] += b[i] * b[i] * w[i]; + } + + const mean = sum.map(v => v / weight); + // per-axis squared error, weighted the same way as color distances + const error = sq.map((v, k) => (v - sum[k] * mean[k]) * [WR, WG, WB][k]); + const axis = error.indexOf(Math.max(...error)); + + return { members, weight, mean, axis, error: error[0] + error[1] + error[2] }; +} + +// stable bottom-up merge sort of indices by their keys, in place +function sortBy(indices: Int32Array, keys: Float64Array, scratch: Int32Array): void { + const n = indices.length; + const RUN = 16; + + // insertion sort short runs + for (let start = 0; start < n; start += RUN) { + const end = Math.min(n, start + RUN); + for (let i = start + 1; i < end; i++) { + const v = indices[i], key = keys[v]; + let j = i - 1; + for (; j >= start && keys[indices[j]] > key; j--) indices[j + 1] = indices[j]; + indices[j + 1] = v; + } + } + + let src = indices, dst = scratch.subarray(0, n); + for (let width = RUN; width < n; width *= 2) { + for (let start = 0; start < n; start += 2 * width) { + const mid = Math.min(n, start + width), end = Math.min(n, start + 2 * width); + let i = start, j = mid, o = start; + // already ordered halves are copied as is + if (mid >= end || keys[src[mid - 1]] <= keys[src[mid]]) { + dst.set(src.subarray(start, end), start); + continue; + } + while (i < mid && j < end) dst[o++] = keys[src[j]] < keys[src[i]] ? src[j++] : src[i++]; + while (i < mid) dst[o++] = src[i++]; + while (j < end) dst[o++] = src[j++]; + } + [src, dst] = [dst, src]; + } + + if (src !== indices) indices.set(src); +} + +/** + * Returns a function finding the palette entry closest to a color, with the same result as a linear scan + * (ties go to the lowest index). Entries are sorted along the axis with the largest weighted spread and + * searched outwards from the color's position, until that axis' distance alone exceeds the best match + */ +function nearestSearch(palette: ArrayLike, entries: number): (r: number, g: number, b: number) => number { + const weights = [WR, WG, WB]; + let axis = 0, spread = -1; + for (let k = 0; k < 3; k++) { + let min = Infinity, max = -Infinity; + for (let i = 0; i < entries; i++) { + min = Math.min(min, palette[i * 3 + k]); + max = Math.max(max, palette[i * 3 + k]); + } + if ((max - min) * weights[k] > spread) { + spread = (max - min) * weights[k]; + axis = k; + } + } + + const order = Int32Array.from({ length: entries }, (_, i) => i).sort((a, b) => palette[a * 3 + axis] - palette[b * 3 + axis]); + const keys = Float64Array.from(order, i => palette[i * 3 + axis]); + const weight = weights[axis]; + + return (r: number, g: number, b: number): number => { + const key = axis === 0 ? r : axis === 1 ? g : b; + let best = 0, bestDistance = Infinity; + + const test = (i: number): void => { + const dr = r - palette[i * 3], dg = g - palette[i * 3 + 1], db = b - palette[i * 3 + 2]; + const distance = WR * dr * dr + WG * dg * dg + WB * db * db; + if (distance < bestDistance || (distance === bestDistance && i < best)) { + bestDistance = distance; + best = i; + } + }; + + // first entry with a key >= the color's + let lo = 0, hi = entries; + while (lo < hi) { + const mid = lo + hi >> 1; + if (keys[mid] < key) lo = mid + 1; + else hi = mid; + } + + // the axis term alone bounds the distance, equal bounds can still win a tie + for (let up = lo, down = lo - 1; up < entries || down >= 0;) { + if (up < entries) { + const d = key - keys[up]; + if (weight * d * d > bestDistance) up = entries; + else test(order[up++]); + } + if (down >= 0) { + const d = key - keys[down]; + if (weight * d * d > bestDistance) down = -1; + else test(order[down--]); + } + } + + return best; + }; +} + +// caches nearest palette lookups on an 18-bit color key +function createLookup(palette: Uint8Array, entries: number): (r: number, g: number, b: number) => number { + const cache = new Int16Array(1 << 18).fill(-1); + const nearest = nearestSearch(palette, entries); + + return (r: number, g: number, b: number): number => { + const key = (r >> 2) << 12 | (g >> 2) << 6 | b >> 2; + let index = cache[key]; + if (index === -1) index = cache[key] = nearest(r, g, b); + return index; + }; +} + +// serpentine floyd-steinberg error diffusion +function ditherMap(pixels: Uint8Array, width: number, palette: Uint8Array, entries: number): Uint8Array { + const height = (pixels.length >> 2) / width; + const lookup = createLookup(palette, entries); + const indices = new Uint8Array(pixels.length >> 2); + + let current = new Float32Array((width + 2) * 3); + let next = new Float32Array((width + 2) * 3); + const strength = 0.85; + + for (let y = 0; y < height; y++) { + const reverse = y & 1; + + for (let step = 0; step < width; step++) { + const x = reverse ? width - 1 - step : step; + const p = y * width + x; + const e = (x + 1) * 3; + + if (pixels[p * 4 + 3] < 128) continue; + + const r = clamp(pixels[p * 4] + current[e] * strength); + const g = clamp(pixels[p * 4 + 1] + current[e + 1] * strength); + const b = clamp(pixels[p * 4 + 2] + current[e + 2] * strength); + + const index = indices[p] = lookup(r, g, b); + const er = r - palette[index * 3]; + const eg = g - palette[index * 3 + 1]; + const eb = b - palette[index * 3 + 2]; + + const forward = reverse ? -3 : 3; + diffuse(current, next, e, forward, er); + diffuse(current, next, e + 1, forward, eg); + diffuse(current, next, e + 2, forward, eb); + } + + [current, next] = [next, current]; + next.fill(0); + } + + return indices; +} + +// floyd-steinberg weights: 7/16 ahead, 3/16 behind below, 5/16 below, 1/16 ahead below +function diffuse(current: Float32Array, next: Float32Array, i: number, forward: number, error: number): void { + current[i + forward] += error * 7 / 16; + next[i - forward] += error * 3 / 16; + next[i] += error * 5 / 16; + next[i + forward] += error / 16; +} + +function clamp(v: number): number { + return v < 0 ? 0 : v > 255 ? 255 : v | 0; +} + diff --git a/src/codecs/tiff.ts b/src/codecs/tiff.ts new file mode 100644 index 0000000..48cedbf --- /dev/null +++ b/src/codecs/tiff.ts @@ -0,0 +1,613 @@ +import { decompressAsync } from './zlib.ts'; +import * as jpeg from './jpeg.ts'; + +const TYPE_SIZES: Record = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8, 13: 4, 16: 8, 17: 8, 18: 8 }; + +const TAGS = { + ImageWidth: 256, + ImageLength: 257, + BitsPerSample: 258, + Compression: 259, + Photometric: 262, + FillOrder: 266, + Orientation: 274, + StripOffsets: 273, + SamplesPerPixel: 277, + RowsPerStrip: 278, + StripByteCounts: 279, + PlanarConfiguration: 284, + Predictor: 317, + ColorMap: 320, + TileWidth: 322, + TileLength: 323, + TileOffsets: 324, + TileByteCounts: 325, + ExtraSamples: 338, + SampleFormat: 339, + JPEGTables: 347, + YCbCrCoefficients: 529, + YCbCrSubSampling: 530, + ReferenceBlackWhite: 532 +}; + +const COMPRESSION_NAMES: Record = { 2: 'CCITT RLE', 3: 'CCITT fax 3', 4: 'CCITT fax 4', 6: 'old-style JPEG', 32809: 'ThunderScan', 34676: 'SGI LogLuv', 34677: 'SGI LogL' }; + +/** + * Decodes the first image of a TIFF into 8-bit RGBA pixel data + * Supports classic and BigTIFF in both byte orders, strips and tiles, chunky and planar data, + * no/LZW/deflate/PackBits/JPEG compression, horizontal and floating point predictors, + * 1-32 bit integer and 32/64 bit float samples (auto-ranged), greyscale, palette, RGB, CMYK and YCbCr images with alpha + * @param {Uint8Array} data + * @return {{width: number, height: number, pixels: Uint8Array}} + */ +type ValuesOf = (tag: number, fallback: number[]) => number[]; + +interface SampleReader { + (rowOffset: number, n: number): { raw: number, value: number }; +} + +export async function decode(data: Uint8Array): Promise<{ width: number, height: number, pixels: Uint8Array, orientation: number }> { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + + let little: boolean; + if (data[0] === 0x49 && data[1] === 0x49) little = true; + else if (data[0] === 0x4d && data[1] === 0x4d) little = false; + else throw new Error('TIFF: invalid byte order'); + + const u16 = (offset: number): number => view.getUint16(offset, little); + const u32 = (offset: number): number => view.getUint32(offset, little); + const u64 = (offset: number): number => { + const lo = view.getUint32(offset + (little ? 0 : 4), little); + const hi = view.getUint32(offset + (little ? 4 : 0), little); + return hi * 0x100000000 + lo; + }; + + const version = u16(2); + if (version !== 42 && version !== 43) throw new Error('TIFF: invalid version'); + const big = version === 43; + + const ifdOffset = big ? u64(8) : u32(4); + if (ifdOffset + 2 > data.length) throw new Error('TIFF: invalid IFD offset'); + + // read the first IFD + const tags = new Map(); + const entryCount = big ? u64(ifdOffset) : u16(ifdOffset); + const entrySize = big ? 20 : 12; + const inlineSize = big ? 8 : 4; + + for (let i = 0; i < entryCount; i++) { + const entry = ifdOffset + (big ? 8 : 2) + i * entrySize; + if (entry + entrySize > data.length) break; + + const tag = u16(entry); + const type = u16(entry + 2); + const count = big ? u64(entry + 4) : u32(entry + 4); + const size = TYPE_SIZES[type]; + if (!size) continue; + + const valueField = entry + (big ? 12 : 8); + const offset = size * count <= inlineSize ? valueField : big ? u64(valueField) : u32(valueField); + if (offset + size * count > data.length) continue; + + tags.set(tag, { type, count, offset }); + } + + const read = (type: number, offset: number): number => { + switch (type) { + case 1: case 2: case 7: return data[offset]; + case 6: return view.getInt8(offset); + case 3: return u16(offset); + case 8: return view.getInt16(offset, little); + case 4: case 13: return u32(offset); + case 9: return view.getInt32(offset, little); + case 5: return u32(offset) / u32(offset + 4); + case 10: return view.getInt32(offset, little) / view.getInt32(offset + 4, little); + case 11: return view.getFloat32(offset, little); + case 12: return view.getFloat64(offset, little); + case 16: case 18: return u64(offset); + case 17: return Number(view.getBigInt64(offset, little)); + default: return 0; + } + }; + + function values(tag: number): number[] | undefined; + function values(tag: number, fallback: number[]): number[]; + function values(tag: number, fallback?: number[]): number[] | undefined { + const entry = tags.get(tag); + if (!entry) return fallback; + + const result: number[] = new Array(entry.count); + const size = TYPE_SIZES[entry.type]; + for (let i = 0; i < entry.count; i++) result[i] = read(entry.type, entry.offset + i * size); + return result; + } + + function value(tag: number): number | undefined; + function value(tag: number, fallback: number): number; + function value(tag: number, fallback?: number): number | undefined { + const v = values(tag); + return v && v.length ? v[0] : fallback; + } + + const orientationTag = value(TAGS.Orientation, 1); + const orientation = orientationTag >= 1 && orientationTag <= 8 ? orientationTag : 1; + const width = value(TAGS.ImageWidth); + const height = value(TAGS.ImageLength); + if (!width || !height) throw new Error('TIFF: invalid dimensions'); + + const samplesPerPixel = value(TAGS.SamplesPerPixel, 1); + const bitsPerSample = values(TAGS.BitsPerSample, [1]); + const bps = bitsPerSample[0]; + if (bitsPerSample.some(b => b !== bps)) throw new Error('TIFF: mixed bit depths are not supported'); + + const compression = value(TAGS.Compression, 1); + const photometric = value(TAGS.Photometric, samplesPerPixel >= 3 ? 2 : 1); + const planar = value(TAGS.PlanarConfiguration, 1) === 2 && samplesPerPixel > 1; + const predictor = value(TAGS.Predictor, 1); + const sampleFormat = value(TAGS.SampleFormat, 1); + const fillOrder = value(TAGS.FillOrder, 1); + const extraSamples = values(TAGS.ExtraSamples, []); + + if (COMPRESSION_NAMES[compression]) throw new Error(`TIFF: ${COMPRESSION_NAMES[compression]} compression is not supported`); + if (![1, 5, 7, 8, 32773, 32946].includes(compression)) throw new Error(`TIFF: compression ${compression} is not supported`); + if (![0, 1, 2, 3, 5, 6].includes(photometric)) throw new Error(`TIFF: photometric interpretation ${photometric} is not supported`); + if (sampleFormat === 3 ? bps !== 32 && bps !== 64 : bps < 1 || bps > 32) throw new Error(`TIFF: ${bps}-bit samples are not supported`); + + const colorChannels = ({ 0: 1, 1: 1, 2: 3, 3: 1, 5: 4, 6: 3 } as Record)[photometric]; + if (samplesPerPixel < colorChannels) throw new Error('TIFF: not enough samples per pixel'); + + // the first extra sample is alpha if it is marked as associated (1) or unassociated (2) + const alphaType = samplesPerPixel > colorChannels ? extraSamples[0] || 0 : 0; + const alphaChannel = alphaType === 1 || alphaType === 2 ? colorChannels : -1; + + // pre-6.0 tiled files store tile offsets in the strip tags + const tiled = tags.has(TAGS.TileWidth) && tags.has(TAGS.TileLength); + const segmentWidth = tiled ? value(TAGS.TileWidth) : width; + const segmentHeight = tiled ? value(TAGS.TileLength) : Math.min(height, value(TAGS.RowsPerStrip, height)); + const offsets = values(TAGS.TileOffsets) || values(TAGS.StripOffsets); + const byteCounts = values(TAGS.TileByteCounts) || values(TAGS.StripByteCounts); + if (!offsets || !segmentWidth || !segmentHeight) throw new Error('TIFF: missing image data'); + + const segmentsAcross = Math.ceil(width / segmentWidth); + const segmentsDown = Math.ceil(height / segmentHeight); + const segmentsPerPlane = segmentsAcross * segmentsDown; + const segment = (index: number): Uint8Array => { + const offset = offsets[index]; + const count = byteCounts ? byteCounts[index] : data.length - offset; + if (offset === undefined || offset >= data.length) throw new Error('TIFF: missing image data'); + return data.subarray(offset, Math.min(data.length, offset + count)); + }; + + const pixels = new Uint8Array(width * height * 4); + + // JPEG compressed segments decode straight to RGBA + if (compression === 7) { + const tables = tags.has(TAGS.JPEGTables) ? data.subarray(tags.get(TAGS.JPEGTables).offset, tags.get(TAGS.JPEGTables).offset + tags.get(TAGS.JPEGTables).count) : null; + const transform = photometric === 6 ? 'ycc' : photometric === 2 ? 'rgb' : photometric === 5 ? 'cmyk' : null; + + for (let s = 0; s < segmentsPerPlane; s++) { + let stream = segment(s); + // abbreviated streams: tables (without EOI) + segment (without SOI) + if (tables) { + const joined = new Uint8Array(tables.length - 2 + stream.length - 2); + joined.set(tables.subarray(0, tables.length - 2)); + joined.set(stream.subarray(2), tables.length - 2); + stream = joined; + } + + const decoded = jpeg.decode(stream, { transform }); + blitRGBA(decoded.pixels, decoded.width, decoded.height, pixels, width, height, (s % segmentsAcross) * segmentWidth, (s / segmentsAcross | 0) * segmentHeight); + } + + return { width, height, pixels, orientation }; + } + + // decompress every segment into a contiguous sample buffer (one plane per sample when planar) + const samplesPerRow = planar ? 1 : samplesPerPixel; + const planes = planar ? samplesPerPixel : 1; + const subsampling = photometric === 6 ? values(TAGS.YCbCrSubSampling, [2, 2]) : [1, 1]; + const subsampled = photometric === 6 && (subsampling[0] !== 1 || subsampling[1] !== 1); + + if (subsampled) { + if (tiled || planar || bps !== 8) throw new Error('TIFF: subsampled YCbCr is only supported for 8-bit strips'); + const [h, v] = subsampling; + const unitsPerRow = Math.ceil(width / h); + const unitRowBytes = unitsPerRow * (h * v + 2); + const units = new Uint8Array(Math.ceil(height / v) * unitRowBytes); + + let written = 0; + for (let s = 0; s < segmentsPerPlane && written < units.length; s++) { + const expected = Math.min(units.length - written, Math.ceil(segmentHeight / v) * unitRowBytes); + const raw = await decompressSegment(segment(s), compression, expected); + units.set(raw.subarray(0, expected), written); + written += expected; + } + + ycbcrUnitsToRGBA(units, width, height, h, v, ycbcrConversion(values), pixels); + return { width, height, pixels, orientation }; + } + + const rowBytes = Math.ceil(width * samplesPerRow * bps / 8); + const planeBytes = rowBytes * height; + const raw = new Uint8Array(planeBytes * planes); + const segmentRowBytes = Math.ceil(segmentWidth * samplesPerRow * bps / 8); + + for (let plane = 0; plane < planes; plane++) { + for (let s = 0; s < segmentsPerPlane; s++) { + const sx = (s % segmentsAcross) * segmentWidth; + const sy = (s / segmentsAcross | 0) * segmentHeight; + const rows = tiled ? segmentHeight : Math.min(segmentHeight, height - sy); + const expected = rows * segmentRowBytes; + + const bytes = await decompressSegment(segment(plane * segmentsPerPlane + s), compression, expected); + if (fillOrder === 2) reverseBits(bytes); + if (predictor === 2) undoHorizontalPredictor(bytes, segmentRowBytes, rows, samplesPerRow, bps, little); + else if (predictor === 3) undoFloatingPointPredictor(bytes, segmentRowBytes, rows, samplesPerRow, bps); + + // copy rows, clipping tiles at the image edges + const visibleRows = Math.min(rows, height - sy); + const startBit = sx * samplesPerRow * bps; + const copyBits = Math.min(segmentWidth, width - sx) * samplesPerRow * bps; + + for (let row = 0; row < visibleRows; row++) { + const target = plane * planeBytes + (sy + row) * rowBytes; + if (startBit % 8 === 0) { + const start = target + startBit / 8; + raw.set(bytes.subarray(row * segmentRowBytes, row * segmentRowBytes + Math.ceil(copyBits / 8)).subarray(0, rowBytes - startBit / 8), start); + } else copyBitsUnaligned(bytes, row * segmentRowBytes * 8, raw, target * 8 + startBit, copyBits); + } + } + } + + // read samples into RGBA + // the floating point predictor reassembles samples as little endian + const sampleLittle = predictor === 3 ? true : little; + const readSample = sampleReader(raw, bps, sampleFormat, sampleLittle, sampleFormat === 3 ? floatRange(raw, bps, sampleLittle) : null); + const sampleAt = planar + ? (x: number, y: number, s: number) => readSample(s * planeBytes + y * rowBytes, x) + : (x: number, y: number, s: number) => readSample(y * rowBytes, x * samplesPerPixel + s); + + const colormap = photometric === 3 ? values(TAGS.ColorMap, []) : []; + if (photometric === 3 && colormap.length < 3 << bps) throw new Error('TIFF: missing color map'); + const ycc = photometric === 6 ? ycbcrConversion(values) : null; + + for (let y = 0, o = 0; y < height; y++) { + for (let x = 0; x < width; x++, o += 4) { + if (photometric === 3) { + const index = sampleAt(x, y, 0).raw; + const entries = 1 << bps; + pixels[o] = colormap[index] >> 8; + pixels[o + 1] = colormap[entries + index] >> 8; + pixels[o + 2] = colormap[2 * entries + index] >> 8; + } else if (photometric <= 1) { + const v = sampleAt(x, y, 0).value; + pixels[o] = pixels[o + 1] = pixels[o + 2] = photometric === 0 ? 255 - v : v; + } else if (photometric === 2) { + pixels[o] = sampleAt(x, y, 0).value; + pixels[o + 1] = sampleAt(x, y, 1).value; + pixels[o + 2] = sampleAt(x, y, 2).value; + } else if (photometric === 5) { + const k = sampleAt(x, y, 3).value; + pixels[o] = cmykToRGB(sampleAt(x, y, 0).value, k); + pixels[o + 1] = cmykToRGB(sampleAt(x, y, 1).value, k); + pixels[o + 2] = cmykToRGB(sampleAt(x, y, 2).value, k); + } else (ycc as YCbCrConverter)(sampleAt(x, y, 0).value, sampleAt(x, y, 1).value, sampleAt(x, y, 2).value, pixels, o); + + if (alphaChannel === -1) pixels[o + 3] = 0xff; + else { + const alpha = pixels[o + 3] = sampleAt(x, y, alphaChannel).value; + // associated alpha is premultiplied + if (alphaType === 1) { + for (let c = 0; c < 3; c++) + pixels[o + c] = alpha ? Math.min(255, Math.round(pixels[o + c] * 255 / alpha)) : 0; + } + } + } + } + + return { width, height, pixels, orientation }; +} + +// returns a function reading the n-th sample of a row as {raw, value} with value scaled to 8 bits +function sampleReader(raw: Uint8Array, bps: number, sampleFormat: number, little: boolean, range: [number, number] | null): SampleReader { + const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); + const result = { raw: 0, value: 0 }; + + if (sampleFormat === 3) { + const [min, max] = range || [0, 1]; + const scale = 255 / (max - min || 1); + + return (rowOffset, n) => { + const v = bps === 32 ? view.getFloat32(rowOffset + n * 4, little) : view.getFloat64(rowOffset + n * 8, little); + result.raw = v; + result.value = v === v ? Math.min(255, Math.max(0, Math.round((v - min) * scale))) : 0; + return result; + }; + } + + const signed = sampleFormat === 2; + const max = 2 ** bps - 1; + + if (bps === 8) { + return (rowOffset, n) => { + result.raw = raw[rowOffset + n]; + result.value = signed ? result.raw ^ 0x80 : result.raw; + return result; + }; + } + + if (bps === 16) { + return (rowOffset, n) => { + result.raw = view.getUint16(rowOffset + n * 2, little); + result.value = (signed ? result.raw ^ 0x8000 : result.raw) >> 8; + return result; + }; + } + + if (bps === 32) { + return (rowOffset, n) => { + result.raw = view.getUint32(rowOffset + n * 4, little); + result.value = (signed ? (result.raw ^ 0x80000000) >>> 0 : result.raw) >>> 24; + return result; + }; + } + + // arbitrary bit depths are packed MSB first + return (rowOffset, n) => { + let bit = rowOffset * 8 + n * bps; + let v = 0; + for (let remaining = bps; remaining > 0;) { + const byte = raw[bit >> 3]; + const available = 8 - (bit & 7); + const take = Math.min(available, remaining); + v = v * (1 << take) + (byte >> available - take & (1 << take) - 1); + bit += take; + remaining -= take; + } + + if (signed) v = (v + 2 ** (bps - 1)) % 2 ** bps; + result.raw = v; + result.value = bps === 1 ? v * 255 : Math.round(v * 255 / max); + return result; + }; +} + +// floats have no fixed range: [0, 1] and [0, 255] data is used as is, anything else is normalized to its min..max +function floatRange(raw: Uint8Array, bps: number, little: boolean): [number, number] { + const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength); + const size = bps / 8; + let min = Infinity, max = -Infinity; + + for (let o = 0; o + size <= raw.length; o += size) { + const v = size === 4 ? view.getFloat32(o, little) : view.getFloat64(o, little); + if (v < min) min = v; + if (v > max) max = v; + } + + if (min >= 0 && max <= 1) return [0, 1]; + if (min >= 0 && max <= 255) return [0, 255]; + return [min, max]; +} + +async function decompressSegment(bytes: Uint8Array, compression: number, expected: number): Promise { + let out: Uint8Array; + if (compression === 5) out = lzw(bytes, expected); + else if (compression === 8 || compression === 32946) out = await decompressAsync(bytes, expected); + else if (compression === 32773) out = packBits(bytes, expected); + // uncompressed segments are views into the input, copy them as predictors and fill order are undone in place + else out = bytes.slice(0, expected); + + if (out.length >= expected) return out.length === expected ? out : out.slice(0, expected); + + // short segments are padded with zeros + const padded = new Uint8Array(expected); + padded.set(out); + return padded; +} + +function lzw(input: Uint8Array, expected: number): Uint8Array { + const out = new Uint8Array(expected); + // pre-5.0 libtiff wrote LSB first codes without early change + const compat = input[0] === 0 && (input[1] & 1) === 1; + + const prefix = new Uint16Array(4096); + const suffix = new Uint8Array(4096); + const first = new Uint8Array(4096); + const lengths = new Uint16Array(4096); + for (let i = 0; i < 256; i++) { + suffix[i] = first[i] = i; + lengths[i] = 1; + } + + let codeSize = 9, next = 258, previous = -1; + let buffer = 0, bits = 0, position = 0, written = 0; + + while (written < expected) { + while (bits < codeSize) { + if (position >= input.length) return out.subarray(0, written); + buffer = compat ? buffer | input[position++] << bits : (buffer << 8 | input[position++]) & 0xffffff; + bits += 8; + } + + let code; + if (compat) { + code = buffer & (1 << codeSize) - 1; + buffer >>>= codeSize; + } else code = buffer >> bits - codeSize & (1 << codeSize) - 1; + bits -= codeSize; + + if (code === 256) { + codeSize = 9; + next = 258; + previous = -1; + continue; + } + + if (code === 257) break; + + let entry; + if (previous === -1) { + if (code > 255) break; + entry = code; + } else if (code < next) entry = code; + else if (code === next) entry = previous; + else break; // invalid code + + const length = lengths[entry]; + let c = entry; + for (let i = written + length - 1; i >= written; i--) { + if (i < expected) out[i] = suffix[c]; + c = prefix[c]; + } + written += length; + + if (code === next && written < expected) out[written] = first[previous]; + if (code === next) written++; + + if (previous !== -1 && next < 4096) { + prefix[next] = previous; + suffix[next] = first[code === next ? previous : code]; + first[next] = first[previous]; + lengths[next] = lengths[previous] + 1; + next++; + } + + // new style LZW switches code size one code early + if (next + (compat ? 0 : 1) >= 1 << codeSize && codeSize < 12) codeSize++; + previous = code; + } + + return out.subarray(0, Math.min(written, expected)); +} + +function packBits(input: Uint8Array, expected: number): Uint8Array { + const out = new Uint8Array(expected); + let i = 0, o = 0; + + while (i < input.length && o < expected) { + const n = input[i++]; + + if (n < 128) { + const count = Math.min(n + 1, expected - o, input.length - i); + out.set(input.subarray(i, i + count), o); + i += n + 1; + o += count; + } else if (n > 128) { + const count = Math.min(257 - n, expected - o); + out.fill(input[i++], o, o + count); + o += count; + } + } + + return out.subarray(0, o); +} + +function reverseBits(bytes: Uint8Array): void { + for (let i = 0; i < bytes.length; i++) { + let b = bytes[i]; + b = (b & 0xf0) >> 4 | (b & 0x0f) << 4; + b = (b & 0xcc) >> 2 | (b & 0x33) << 2; + bytes[i] = (b & 0xaa) >> 1 | (b & 0x55) << 1; + } +} + +function undoHorizontalPredictor(bytes: Uint8Array, rowBytes: number, rows: number, samplesPerPixel: number, bps: number, little: boolean): void { + if (bps === 8) { + for (let r = 0; r < rows; r++) + for (let i = r * rowBytes + samplesPerPixel, end = (r + 1) * rowBytes; i < end; i++) + bytes[i] = bytes[i] + bytes[i - samplesPerPixel]; + } else if (bps === 16 || bps === 32) { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const size = bps / 8; + const get = size === 2 ? (o: number): number => view.getUint16(o, little) : (o: number): number => view.getUint32(o, little); + const set = size === 2 ? (o: number, v: number): void => view.setUint16(o, v, little) : (o: number, v: number): void => view.setUint32(o, v >>> 0, little); + const stride = samplesPerPixel * size; + + for (let r = 0; r < rows; r++) + for (let o = r * rowBytes + stride, end = r * rowBytes + Math.floor(rowBytes / size) * size; o < end; o += size) + set(o, get(o) + get(o - stride)); + } else throw new Error(`TIFF: horizontal predictor with ${bps}-bit samples is not supported`); +} + +// floating point predictor: byte-wise differencing over byte planes (most significant first) +function undoFloatingPointPredictor(bytes: Uint8Array, rowBytes: number, rows: number, samplesPerPixel: number, bps: number): void { + const size = bps / 8; + const count = rowBytes / size; + const tmp = new Uint8Array(rowBytes); + + for (let r = 0; r < rows; r++) { + const row = bytes.subarray(r * rowBytes, (r + 1) * rowBytes); + for (let i = samplesPerPixel; i < rowBytes; i++) row[i] = row[i] + row[i - samplesPerPixel]; + tmp.set(row); + + // reassemble the byte planes into little endian samples + for (let n = 0; n < count; n++) + for (let b = 0; b < size; b++) + row[n * size + b] = tmp[(size - b - 1) * count + n]; + } +} + +function copyBitsUnaligned(src: Uint8Array, srcBit: number, dst: Uint8Array, dstBit: number, count: number): void { + for (let i = 0; i < count; i++, srcBit++, dstBit++) { + const bit = src[srcBit >> 3] >> 7 - (srcBit & 7) & 1; + if (bit) dst[dstBit >> 3] |= 0x80 >> (dstBit & 7); + else dst[dstBit >> 3] &= ~(0x80 >> (dstBit & 7)); + } +} + +function blitRGBA(src: Uint8Array, sw: number, sh: number, dst: Uint8Array, dw: number, dh: number, x0: number, y0: number): void { + const w = Math.min(sw, dw - x0); + for (let y = 0; y < sh && y0 + y < dh; y++) + dst.set(src.subarray(y * sw * 4, (y * sw + w) * 4), ((y0 + y) * dw + x0) * 4); +} + +type YCbCrConverter = (Y: number, Cb: number, Cr: number, out: Uint8Array, o: number) => void; + +function ycbcrConversion(values: ValuesOf): YCbCrConverter { + const [lr, lg, lb] = values(TAGS.YCbCrCoefficients, [0.299, 0.587, 0.114]); + const reference = values(TAGS.ReferenceBlackWhite, [0, 255, 128, 255, 128, 255]); + + const scale = (v: number, black: number, white: number, center: boolean): number => (v - black) * (center ? 127 : 255) / (white - black || 1); + + return (Y: number, Cb: number, Cr: number, out: Uint8Array, o: number): void => { + const y = scale(Y, reference[0], reference[1], false); + const cb = scale(Cb, reference[2], reference[3], true); + const cr = scale(Cr, reference[4], reference[5], true); + + const r = y + cr * (2 - 2 * lr); + const b = y + cb * (2 - 2 * lb); + const g = (y - lb * b - lr * r) / lg; + + out[o] = Math.min(255, Math.max(0, Math.round(r))); + out[o + 1] = Math.min(255, Math.max(0, Math.round(g))); + out[o + 2] = Math.min(255, Math.max(0, Math.round(b))); + }; +} + +function ycbcrUnitsToRGBA(units: Uint8Array, width: number, height: number, h: number, v: number, convert: YCbCrConverter, pixels: Uint8Array): void { + const unitsPerRow = Math.ceil(width / h); + const unitSize = h * v + 2; + + for (let uy = 0; uy < Math.ceil(height / v); uy++) + for (let ux = 0; ux < unitsPerRow; ux++) { + const u = (uy * unitsPerRow + ux) * unitSize; + const cb = units[u + h * v], cr = units[u + h * v + 1]; + + for (let j = 0; j < v; j++) + for (let i = 0; i < h; i++) { + const x = ux * h + i, y = uy * v + j; + if (x >= width || y >= height) continue; + + const o = (y * width + x) * 4; + convert(units[u + j * h + i], cb, cr, pixels, o); + pixels[o + 3] = 0xff; + } + } +} + +function cmykToRGB(c: number, k: number): number { + const t = (255 - c) * k + 128; + const v = 255 - (c + ((t >> 8) + t >> 8)); + return v < 0 ? 0 : v; +} + diff --git a/src/codecs/zlib.ts b/src/codecs/zlib.ts new file mode 100644 index 0000000..5d2539b --- /dev/null +++ b/src/codecs/zlib.ts @@ -0,0 +1,717 @@ +/*! + * zlib deflate/inflate, derived from fflate (https://github.com/101arrowz/fflate) + * Copyright (c) 2020 Arjun Barrett, MIT License (see THIRD_PARTY_NOTICES.md) + */ + +const u8 = Uint8Array, u16 = Uint16Array, u32 = Uint32Array; + +// code length index map +const clim = u8.of(16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15); +// fixed length extra bits +const fleb = u8.of(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0); +// fixed distance extra bits +const fdeb = u8.of(0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0); + +// base values and reverse lookup for lengths/distances +function freb(eb: Uint8Array, start: number): [Uint16Array, Uint32Array] { + const b = new u16(31); + for (let i = 0; i < 31; ++i) b[i] = start += 1 << eb[i - 1]; + + const r = new u32(b[30]); + for (let i = 1; i < 30; ++i) + for (let j = b[i]; j < b[i + 1]; ++j) r[j] = j - b[i] << 5 | i; + + return [b, r]; +} + +const [fl, revfl] = freb(fleb, 2); +fl[28] = 258; +revfl[258] = 28; +const [fd, revfd] = freb(fdeb, 0); + +// bit reversal map +const rev = new u16(32768); +for (let i = 0; i < 32768; ++i) { + let x = (i & 0xaaaa) >>> 1 | (i & 0x5555) << 1; + x = (x & 0xcccc) >>> 2 | (x & 0x3333) << 2; + x = (x & 0xf0f0) >>> 4 | (x & 0x0f0f) << 4; + rev[i] = ((x & 0xff00) >>> 8 | (x & 0x00ff) << 8) >>> 1; +} + +// huffman tree from code lengths, as a reverse lookup map (r) or a code map +function hMap(cd: Uint8Array, mb: number, r: boolean): Uint16Array { + const s = cd.length; + const l = new u16(mb); + for (let i = 0; i < s; ++i) if (cd[i]) ++l[cd[i] - 1]; + + const le = new u16(mb); + for (let i = 1; i < mb; ++i) le[i] = le[i - 1] + l[i - 1] << 1; + + let co: Uint16Array; + if (r) { + co = new u16(1 << mb); + const rvb = 15 - mb; + for (let i = 0; i < s; ++i) { + if (!cd[i]) continue; + const sv = i << 4 | cd[i]; + const r1 = mb - cd[i]; + let v = le[cd[i] - 1]++ << r1; + for (const m = v | (1 << r1) - 1; v <= m; ++v) co[rev[v] >>> rvb] = sv; + } + } else { + co = new u16(s); + for (let i = 0; i < s; ++i) + if (cd[i]) co[i] = rev[le[cd[i] - 1]++] >>> 15 - cd[i]; + } + + return co; +} + +// fixed length/distance trees +const flt = new u8(288); +for (let i = 0; i < 144; ++i) flt[i] = 8; +for (let i = 144; i < 256; ++i) flt[i] = 9; +for (let i = 256; i < 280; ++i) flt[i] = 7; +for (let i = 280; i < 288; ++i) flt[i] = 8; +const fdt = new u8(32); +for (let i = 0; i < 32; ++i) fdt[i] = 5; +const flm = hMap(flt, 9, false), flrm = hMap(flt, 9, true); +const fdm = hMap(fdt, 5, false), fdrm = hMap(fdt, 5, true); + +function max(a: Uint8Array): number { + let m = a[0]; + for (let i = 1; i < a.length; ++i) if (a[i] > m) m = a[i]; + return m; +} + +// read d at bit position p with mask m +const bits = (d: Uint8Array, p: number, m: number): number => { + const o = p / 8 | 0; + return (d[o] | d[o + 1] << 8) >> (p & 7) & m; +}; + +// read d at bit position p, at least 16 bits +const bits16 = (d: Uint8Array, p: number): number => { + const o = p / 8 | 0; + return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); +}; + +// byte position after bit position p +const shft = (p: number): number => (p + 7) / 8 | 0; + +/** + * Inflates raw deflate data into a buffer of a fixed size; output beyond it is discarded + */ +function inflate(dat: Uint8Array, buf: Uint8Array): Uint8Array { + const sl = dat.length; + if (!sl) return buf.subarray(0, 0); + + const tbts = sl * 8; + let final = 0, pos = 0, bt = 0; + let lm: Uint16Array | null = null, dm: Uint16Array | null = null, lbt = 0, dbt = 0; + + do { + if (!lm) { + final = bits(dat, pos, 1); + const type = bits(dat, pos + 1, 3); + pos += 3; + + if (!type) { + // stored block + const s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; + if (t > sl) throw new Error('zlib: unexpected end of data'); + if (bt < buf.length) buf.set(dat.subarray(s, Math.min(t, s + buf.length - bt)), bt); + bt += l; + pos = t * 8; + continue; + } else if (type === 1) { + lm = flrm; + dm = fdrm; + lbt = 9; + dbt = 5; + } else if (type === 2) { + // dynamic huffman tables + const hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + const tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + + const ldt = new u8(tl); + const clt = new u8(19); + for (let i = 0; i < hcLen; ++i) clt[clim[i]] = bits(dat, pos + i * 3, 7); + pos += hcLen * 3; + + const clb = max(clt), clbmsk = (1 << clb) - 1; + const clm = hMap(clt, clb, true); + for (let i = 0; i < tl;) { + const r = clm[bits(dat, pos, clbmsk)]; + pos += r & 15; + const s = r >> 4; + + if (s < 16) ldt[i++] = s; + else { + let c = 0, n = 0; + if (s === 16) { + n = 3 + bits(dat, pos, 3); + pos += 2; + c = ldt[i - 1]; + } else if (s === 17) { + n = 3 + bits(dat, pos, 7); + pos += 3; + } else if (s === 18) { + n = 11 + bits(dat, pos, 127); + pos += 7; + } + while (n--) ldt[i++] = c; + } + } + + const lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + lbt = max(lt); + dbt = max(dt); + lm = hMap(lt, lbt, true); + dm = hMap(dt, dbt, true); + } else throw new Error('zlib: invalid block type'); + + if (pos > tbts) throw new Error('zlib: unexpected end of data'); + } + + const lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + for (; ;) { + const c = lm[bits16(dat, pos) & lms], sym = c >> 4; + pos += c & 15; + if (pos > tbts) throw new Error('zlib: unexpected end of data'); + if (!c) throw new Error('zlib: invalid length/literal'); + + if (sym < 256) buf[bt++] = sym; + else if (sym === 256) { + lm = null; + break; + } else { + let add = sym - 254; + if (sym > 264) { + const i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + + const d = (dm as Uint16Array)[bits16(dat, pos) & dms], dsym = d >> 4; + if (!d) throw new Error('zlib: invalid distance'); + pos += d & 15; + + let dt = fd[dsym]; + if (dsym > 3) { + const b = fdeb[dsym]; + dt += bits16(dat, pos) & (1 << b) - 1; + pos += b; + } + + if (pos > tbts) throw new Error('zlib: unexpected end of data'); + if (bt >= buf.length) { + bt += add; + continue; + } + + const end = bt + add; + for (; bt < end; bt += 4) { + buf[bt] = buf[bt - dt]; + buf[bt + 1] = buf[bt + 1 - dt]; + buf[bt + 2] = buf[bt + 2 - dt]; + buf[bt + 3] = buf[bt + 3 - dt]; + } + bt = end; + } + } + } while (!final); + + return buf.subarray(0, Math.min(bt, buf.length)); +} + +// write bits starting at bit position p +const wbits = (d: Uint8Array, p: number, v: number): void => { + v <<= p & 7; + const o = p / 8 | 0; + d[o] |= v; + d[o + 1] |= v >> 8; +}; + +// write at least 16 bits starting at bit position p +const wbits16 = (d: Uint8Array, p: number, v: number): void => { + v <<= p & 7; + const o = p / 8 | 0; + d[o] |= v; + d[o + 1] |= v >> 8; + d[o + 2] |= v >> 16; +}; + +interface HuffNode { + s: number; + f: number; + l?: HuffNode; + r?: HuffNode; +} + +const et = new u8(0); + +// creates code lengths from a frequency table, limited to mb bits +function hTree(d: Uint16Array, mb: number): [Uint8Array, number] { + const t: HuffNode[] = []; + for (let i = 0; i < d.length; ++i) if (d[i]) t.push({ s: i, f: d[i] }); + + const s = t.length; + const t2 = t.slice(); + if (!s) return [et, 0]; + if (s === 1) { + const v = new u8(t[0].s + 1); + v[t[0].s] = 1; + return [v, 1]; + } + + t.sort((a, b) => a.f - b.f); + // after i2 reaches the last ind, will be stopped + t.push({ s: -1, f: 25001 }); + + let l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; + t[0] = { s: -1, f: l.f + r.f, l, r }; + // efficient algorithm from UZIP.js + while (i1 !== s - 1) { + l = t[t[i0].f < t[i2].f ? i0++ : i2++]; + r = t[i0 !== i1 && t[i0].f < t[i2].f ? i0++ : i2++]; + t[i1++] = { s: -1, f: l.f + r.f, l, r }; + } + + let maxSym = t2[0].s; + for (let i = 1; i < s; ++i) if (t2[i].s > maxSym) maxSym = t2[i].s; + + const tr = new u16(maxSym + 1); + let mbt = ln(t[i1 - 1], tr, 0); + if (mbt > mb) { + // more algorithms from UZIP.js, this one limits the code lengths + let i = 0, dt = 0; + const lft = mbt - mb, cst = 1 << lft; + t2.sort((a, b) => tr[b.s] - tr[a.s] || a.f - b.f); + + for (; i < s; ++i) { + const i2 = t2[i].s; + if (tr[i2] > mb) { + dt += cst - (1 << mbt - tr[i2]); + tr[i2] = mb; + } else break; + } + + dt >>= lft; + while (dt > 0) { + const i2 = t2[i].s; + if (tr[i2] < mb) dt -= 1 << mb - tr[i2]++ - 1; + else ++i; + } + + for (; i >= 0 && dt; --i) { + const i2 = t2[i].s; + if (tr[i2] === mb) { + --tr[i2]; + ++dt; + } + } + + mbt = mb; + } + + return [new u8(tr), mbt]; +} + +// assigns depths to the leaves of a huffman tree, returns the maximum depth +function ln(n: HuffNode, l: Uint16Array, d: number): number { + return n.s === -1 + ? Math.max(ln(n.l as HuffNode, l, d + 1), ln(n.r as HuffNode, l, d + 1)) + : (l[n.s] = d); +} + +// run length encodes code lengths +function lc(c: Uint8Array): [Uint16Array, number] { + let s = c.length; + // trailing zeros don't need to be written + while (s && !c[--s]); + + const cl = new u16(++s); + let cli = 0, cln = c[0], cls = 1; + const w = (v: number): void => { + cl[cli++] = v; + }; + + for (let i = 1; i <= s; ++i) { + if (c[i] === cln && i !== s) ++cls; + else { + if (!cln && cls > 2) { + for (; cls > 138; cls -= 138) w(32754); + if (cls > 2) { + w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); + cls = 0; + } + } else if (cls > 3) { + w(cln); + --cls; + for (; cls > 6; cls -= 6) w(8304); + if (cls > 2) { + w(cls - 3 << 5 | 8208); + cls = 0; + } + } + + while (cls--) w(cln); + cls = 1; + cln = c[i]; + } + } + + return [cl.subarray(0, cli), s]; +} + +// length of encoded data given frequencies and code lengths +function clen(cf: Uint16Array, cl: Uint8Array): number { + let l = 0; + for (let i = 0; i < cl.length; ++i) l += cf[i] * cl[i]; + return l; +} + +// writes a stored block +function wfblk(out: Uint8Array, pos: number, dat: Uint8Array): number { + const s = dat.length; + const o = shft(pos + 2); + out[o] = s & 255; + out[o + 1] = s >> 8; + out[o + 2] = out[o] ^ 255; + out[o + 3] = out[o + 1] ^ 255; + for (let i = 0; i < s; ++i) out[o + i + 4] = dat[i]; + return (o + 4 + s) * 8; +} + +// writes a block +function wblk(dat: Uint8Array, out: Uint8Array, final: number, syms: Uint32Array, lf: Uint16Array, df: Uint16Array, eb: number, li: number, bs: number, bl: number, p: number): number { + wbits(out, p++, final); + ++lf[256]; + + const [dlt, mlb] = hTree(lf, 15); + const [ddt, mdb] = hTree(df, 15); + const [lclt, nlc] = lc(dlt); + const [lcdt, ndc] = lc(ddt); + + const lcfreq = new u16(19); + for (let i = 0; i < lclt.length; ++i) ++lcfreq[lclt[i] & 31]; + for (let i = 0; i < lcdt.length; ++i) ++lcfreq[lcdt[i] & 31]; + + const [lct, mlcb] = hTree(lcfreq, 7); + let nlcc = 19; + for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc); + + const flen = (bl + 5) << 3; + const ftlen = clen(lf, flt) + clen(df, fdt) + eb; + const dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]; + if (bs >= 0 && flen <= ftlen && flen <= dtlen) return wfblk(out, p, dat.subarray(bs, bs + bl)); + + let lm: Uint16Array, ll: Uint8Array, dm: Uint16Array, dl: Uint8Array; + wbits(out, p, 1 + (dtlen < ftlen ? 1 : 0)); + p += 2; + + if (dtlen < ftlen) { + lm = hMap(dlt, mlb, false); + ll = dlt; + dm = hMap(ddt, mdb, false); + dl = ddt; + + const llm = hMap(lct, mlcb, false); + wbits(out, p, nlc - 257); + wbits(out, p + 5, ndc - 1); + wbits(out, p + 10, nlcc - 4); + p += 14; + + for (let i = 0; i < nlcc; ++i) wbits(out, p + 3 * i, lct[clim[i]]); + p += 3 * nlcc; + + for (const clct of [lclt, lcdt]) { + for (let i = 0; i < clct.length; ++i) { + const len = clct[i] & 31; + wbits(out, p, llm[len]); + p += lct[len]; + if (len > 15) { + wbits(out, p, clct[i] >> 5 & 127); + p += clct[i] >> 12; + } + } + } + } else { + lm = flm; + ll = flt; + dm = fdm; + dl = fdt; + } + + for (let i = 0; i < li; ++i) { + const sym = syms[i]; + if (sym > 255) { + const len = sym >> 18 & 31; + wbits16(out, p, lm[len + 257]); + p += ll[len + 257]; + if (len > 7) { + wbits(out, p, sym >> 23 & 31); + p += fleb[len]; + } + + const dst = sym & 31; + wbits16(out, p, dm[dst]); + p += dl[dst]; + if (dst > 3) { + wbits16(out, p, sym >> 5 & 8191); + p += fdeb[dst]; + } + } else { + wbits16(out, p, lm[sym]); + p += ll[sym]; + } + } + + wbits16(out, p, lm[256]); + return p + ll[256]; +} + +// deflate options (nice << 13) | chain +const deo = u32.of(65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632); + +// compresses data into raw deflate, leaving pre bytes before and post bytes after the output +function deflate(dat: Uint8Array, lvl: number, plvl: number, pre: number, post: number): Uint8Array { + const s = dat.length; + const o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post); + const w = o.subarray(pre, o.length - post); + let pos = 0; + + if (!lvl || s < 8) { + for (let i = 0; i <= s; i += 65535) { + // end + const e = i + 65535; + if (e >= s) w[pos >> 3] = 1; + pos = wfblk(w, pos + 1, dat.subarray(i, e)); + } + } else { + const opt = deo[lvl - 1]; + const n = opt >> 13, c = opt & 8191; + const msk = (1 << plvl) - 1; + // prev 2-byte val map, curr 2-byte val map + const prev = new u16(32768), head = new u16(msk + 1); + const bs1 = Math.ceil(plvl / 3), bs2 = 2 * bs1; + const hsh = (i: number): number => ((dat[i] | 0) ^ (dat[i + 1] | 0) << bs1 ^ (dat[i + 2] | 0) << bs2) & msk; + + const syms = new u32(25000); + const lf = new u16(288), df = new u16(32); + let lcnt = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; + + // hashes near the end read past the data, which yields zeros + for (; i < s; ++i) { + const hv = hsh(i); + let imod = i & 32767, pimod = head[hv]; + prev[imod] = pimod; + head[hv] = imod; + + if (wi <= i) { + const rem = s - i; + if ((lcnt > 7000 || li > 24576) && rem > 423) { + pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); + li = lcnt = eb = 0; + bs = i; + for (let j = 0; j < 286; ++j) lf[j] = 0; + for (let j = 0; j < 30; ++j) df[j] = 0; + } + + // len, dist, chain + let l = 2, d = 0, ch = c, dif = imod - pimod & 32767; + if (rem > 2 && hv === hsh(i - dif)) { + const maxn = Math.min(n, rem) - 1; + const maxd = Math.min(32767, i); + // max possible length, not capped at dif because decompressors implement "rolling" index population + const ml = Math.min(258, rem); + + while (dif <= maxd && --ch && imod !== pimod) { + if (dat[i + l] === dat[i + l - dif]) { + let nl = 0; + for (; nl < ml && dat[i + nl] === dat[i + nl - dif]; ++nl); + + if (nl > l) { + l = nl; + d = dif; + // break out early when we reach "nice" (we are satisfied enough) + if (nl > maxn) break; + + // now, find the rarest 2-byte sequence within this length of literals and search for that instead + const mmd = Math.min(dif, nl - 2); + let md = 0; + for (let j = 0; j < mmd; ++j) { + const ti = i - dif + j & 32767; + const pti = prev[ti]; + const cd = ti - pti & 32767; + if (cd > md) { + md = cd; + pimod = ti; + } + } + } + } + + // check the previous match + imod = pimod; + pimod = prev[imod]; + dif += imod - pimod & 32767; + } + } + + if (d) { + // store both dist and len data in one int32 + syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; + const lin = revfl[l] & 31, din = revfd[d] & 31; + eb += fleb[lin] + fdeb[din]; + ++lf[257 + lin]; + ++df[din]; + wi = i + l; + ++lcnt; + } else { + syms[li++] = dat[i]; + ++lf[dat[i]]; + } + } + } + + pos = wblk(dat, w, 1, syms, lf, df, eb, li, bs, i - bs, pos); + } + + return o.subarray(0, pre + shft(pos) + post); +} + +function adler32(data: Uint8Array): number { + let a = 1, b = 0; + for (let i = 0; i < data.length;) { + // the sums can't overflow for 2655 bytes, then reduce them + const e = Math.min(i + 2655, data.length); + for (; i < e; ++i) b += a += data[i]; + a = (a & 65535) + 15 * (a >> 16); + b = (b & 65535) + 15 * (b >> 16); + } + + a %= 65521; + b %= 65521; + return (b << 16 | a) >>> 0; +} + +/** + * Compresses data into a zlib stream + * @param level compression level 0-9 + */ +export function compress(data: Uint8Array, level: number): Uint8Array { + level = Math.min(9, Math.max(0, level | 0)); + const memory = Math.ceil(Math.max(8, Math.min(13, Math.log(data.length))) * 1.5); + const out = deflate(data, level, memory, 2, 4); + + // zlib header with the compression level hint + const hint = level === 0 ? 0 : level < 6 ? 1 : level === 9 ? 3 : 2; + out[0] = 120; + out[1] = hint << 6 | (hint ? 32 - 2 * hint : 1); + + const checksum = adler32(data); + const end = out.length - 4; + out[end] = checksum >>> 24; + out[end + 1] = checksum >> 16 & 255; + out[end + 2] = checksum >> 8 & 255; + out[end + 3] = checksum & 255; + return out; +} + +/** + * Decompresses a zlib stream into at most limit bytes + */ +export function decompress(data: Uint8Array, limit: number): Uint8Array { + if ((data[0] & 15) !== 8 || data[0] >> 4 > 7 || (data[0] << 8 | data[1]) % 31) + throw new Error('zlib: invalid header'); + if (data[1] & 32) + throw new Error('zlib: preset dictionaries are not supported'); + + return inflate(data.subarray(2), new u8(limit)); +} + +// native zlib, accessed through feature detection with minimal local types instead of depending on DOM or Node type +// definitions: node:zlib (Node, Bun and Deno expose it through process.getBuiltinModule, without an import) is the +// fastest and honors compression levels; DecompressionStream is part of the web platform + +interface NodeZlib { + deflateSync(data: Uint8Array, options: { level: number }): Uint8Array; + inflateSync(data: Uint8Array, options: { maxOutputLength: number, finishFlush: number }): Uint8Array; + constants: { Z_SYNC_FLUSH: number }; +} + +interface StreamReader { + read(): Promise<{ done: boolean, value?: Uint8Array }>; + cancel(): Promise; +} + +interface Globals { + process?: { getBuiltinModule?: (id: string) => unknown }; + DecompressionStream?: new (format: 'deflate') => unknown; + Blob?: new (parts: Uint8Array[]) => { stream(): { pipeThrough(transform: unknown): { getReader(): StreamReader } } }; +} + +const { process: runtime, DecompressionStream: NativeDecompression, Blob: NativeBlob } = globalThis as unknown as Globals; + +const nodeZlib = ((): NodeZlib | null => { + try { + return (runtime && runtime.getBuiltinModule && runtime.getBuiltinModule('node:zlib') as NodeZlib) || null; + } catch { + return null; + } +})(); + +// plain Uint8Array view of a node Buffer +const plain = (buffer: Uint8Array): Uint8Array => new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + +/** + * Compresses data into a zlib stream, with the runtime's native zlib when available + * @param level compression level 0-9 + */ +export async function compressAsync(data: Uint8Array, level: number): Promise { + level = Math.min(9, Math.max(0, level | 0)); + if (nodeZlib) return plain(nodeZlib.deflateSync(data, { level })); + return compress(data, level); +} + +/** + * Decompresses a zlib stream into at most limit bytes, using the runtime's native zlib when available + */ +export async function decompressAsync(data: Uint8Array, limit: number): Promise { + if (nodeZlib) { + try { + // a sync flush tolerates truncated streams, the output limit protects against zip bombs + return plain(nodeZlib.inflateSync(data, { maxOutputLength: limit, finishFlush: nodeZlib.constants.Z_SYNC_FLUSH })); + } catch { + // e.g. more data than expected, handled below + } + } + + if (!NativeDecompression || !NativeBlob) return decompress(data, limit); + + const out = new Uint8Array(limit); + let offset = 0; + + const reader = new NativeBlob([data]).stream().pipeThrough(new NativeDecompression('deflate')).getReader(); + try { + for (; ;) { + const { done, value } = await reader.read(); + if (done || !value) break; + + out.set(value.subarray(0, Math.max(0, limit - offset)), offset); + offset += value.length; + // stop inflating once the expected size is reached (protects against zip bombs) + if (offset >= limit) break; + } + } catch { + // the native decoder rejects truncated streams and trailing garbage that the JS one tolerates + return decompress(data, limit); + } finally { + reader.cancel().catch(() => { }); + } + + return out.subarray(0, Math.min(offset, limit)); +} diff --git a/src/frame.ts b/src/frame.ts new file mode 100644 index 0000000..36835c6 --- /dev/null +++ b/src/frame.ts @@ -0,0 +1,134 @@ +import { Image, ResizeMode } from './image.ts'; + +/** Names of the GIF frame disposal modes, see {@link Frame.disposalMode} */ +export type FrameDisposalModeName = 'any' | 'keep' | 'previous' | 'background'; +/** Ids of the GIF frame disposal modes as stored in the file (0 any, 1 keep, 2 previous, 3 background) */ +export type FrameDisposalModeId = 0 | 1 | 2 | 3; + +const DISPOSAL_MODES: readonly FrameDisposalModeName[] = ['any', 'keep', 'previous', 'background']; + +/** + * Represents a frame in a GIF + */ +export class Frame extends Image { + /** + * Milliseconds. GIFs store hundredths of a second, so this is rounded to 10 ms when encoding; + * note that browsers play frames shorter than 20 ms with 100 ms + */ + duration: number; + /** The frame's horizontal offset in the GIF */ + xOffset: number; + /** The frame's vertical offset in the GIF */ + yOffset: number; + /** @internal */ + private __disposalMode__: FrameDisposalModeId = 1; + + /** GIF frame disposal mode ANY: the decoder decides (in practice like KEEP). For use with {@link Frame} */ + static get DISPOSAL_ANY(): 'any' { + return 'any'; + } + + /** GIF frame disposal mode KEEP. For use with {@link Frame} */ + static get DISPOSAL_KEEP(): 'keep' { + return 'keep'; + } + + /** GIF frame disposal mode PREVIOUS. For use with {@link Frame} */ + static get DISPOSAL_PREVIOUS(): 'previous' { + return 'previous'; + } + + /** GIF frame disposal mode BACKGROUND. For use with {@link Frame} */ + static get DISPOSAL_BACKGROUND(): 'background' { + return 'background'; + } + + /** @internal */ + private static __convert_disposal_mode__(mode: FrameDisposalModeName | FrameDisposalModeId): FrameDisposalModeId { + const id = typeof mode === 'string' ? DISPOSAL_MODES.indexOf(mode) : mode; + if (!(id >= 0 && id <= 3)) + throw new RangeError('Invalid disposal mode'); + + return id as FrameDisposalModeId; + } + + /** + * Creates a new, blank frame + * @param duration The frame's duration (in ms) + * @param xOffset The frame's offset on the x-axis + * @param yOffset The frame's offset on the y-axis + * @param disposalMode The frame's disposal mode + */ + constructor(width: number, height: number, duration = 100, xOffset = 0, yOffset = 0, disposalMode: FrameDisposalModeName | FrameDisposalModeId = Frame.DISPOSAL_KEEP) { + if (isNaN(duration) || duration < 0) + throw new RangeError('Invalid frame duration'); + + super(width, height); + this.duration = duration; + this.xOffset = xOffset; + this.yOffset = yOffset; + this.disposalMode = disposalMode; + } + + /** The frame's disposal mode (`'any'`, `'keep'`, `'previous'` or `'background'`); also settable by id (0-3) */ + get disposalMode(): FrameDisposalModeName { + return DISPOSAL_MODES[this.__disposalMode__]; + } + + /** + * The frame's disposal mode as id (0 any, 1 keep, 2 previous, 3 background) + * @internal + */ + get __disposal_id__(): FrameDisposalModeId { + return this.__disposalMode__; + } + + /** Sets the disposal mode by name or id (0-3) */ + set disposalMode(disposalMode: FrameDisposalModeName | FrameDisposalModeId) { + this.__disposalMode__ = Frame.__convert_disposal_mode__(disposalMode); + } + + override toString(): string { + return `Frame<${this.width}x${this.height}x${this.duration}ms>`; + } + + /** + * Converts an Image instance to a Frame, cloning it in the process + * @param image The image to create the frame from + * @param duration The frame's duration (in ms) + * @param xOffset The frame's offset on the x-axis + * @param yOffset The frame's offset on the y-axis + * @param disposalMode The frame's disposal mode + */ + static from(image: Image, duration?: number, xOffset?: number, yOffset?: number, disposalMode: FrameDisposalModeName | FrameDisposalModeId = Frame.DISPOSAL_KEEP): Frame { + if (!(image instanceof Image)) + throw new TypeError('Invalid image passed'); + + const frame = new Frame(image.width, image.height, duration, xOffset, yOffset, disposalMode); + frame.bitmap.set(image.bitmap); + + return frame; + } + + /** + * Clones the frame, including its duration, offsets and disposal mode + */ + override clone(): Frame { + return Frame.from(this, this.duration, this.xOffset, this.yOffset, this.disposalMode); + } + + /** + * Resizes the frame, scaling its offsets along with it + */ + override resize(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + const originalWidth = this.width; + const originalHeight = this.height; + + super.resize(width, height, mode); + + this.xOffset = Math.round(this.xOffset * this.width / originalWidth); + this.yOffset = Math.round(this.yOffset * this.height / originalHeight); + + return this; + } +} diff --git a/src/gif.ts b/src/gif.ts new file mode 100644 index 0000000..2418c34 --- /dev/null +++ b/src/gif.ts @@ -0,0 +1,138 @@ +import * as codec from './codecs/gif.ts'; +import { BinaryData, view } from './util/mem.ts'; +import { Image, ResizeMode } from './image.ts'; +import { Frame } from './frame.ts'; +import { ImageType } from './image-type.ts'; + +/** + * Represents a GIF image as an array of frames. + * Array methods like map and filter return plain arrays. + */ +export class GIF extends Array { + /** How many times to repeat the GIF (-1 = forever, 0 = play once) */ + loopCount: number; + + /** + * Creates a new GIF image + * @param frames The frames to create the GIF from + * @param loopCount How many times to repeat the GIF (-1 = forever, 0 = play once) + */ + constructor(frames: Frame[], loopCount = -1) { + super(); + // push instead of super(...frames), so a single frame isn't taken as a length + for (const frame of frames) this.push(frame); + + for (let i = 0; i < this.length; i++) + if (!(this[i] instanceof Frame)) + throw new TypeError(`Frame ${i} is not an instance of Frame`); + + if (loopCount < -1 || isNaN(loopCount)) + throw new RangeError('Invalid loop count'); + + this.loopCount = loopCount; + } + + /** @internal */ + static override get [Symbol.species](): ArrayConstructor { + return Array; + } + + /** The GIF's width */ + get width(): number { + let max = 0; + for (const frame of this) max = Math.max(max, frame.width + frame.xOffset); + return max; + } + + /** The GIF's height */ + get height(): number { + let max = 0; + for (const frame of this) max = Math.max(max, frame.height + frame.yOffset); + return max; + } + + /** The GIF's duration (in ms) */ + get duration(): number { + let duration = 0; + for (const frame of this) duration += frame.duration; + return duration; + } + + override toString(): string { + return `GIF<${this.width}x${this.height}x${this.duration}ms>`; + } + + /** + * Returns a new GIF containing a range of this GIF's frames (same semantics as Array.prototype.slice) + */ + override slice(start?: number, end?: number): GIF { + return new GIF(Array.prototype.slice.call(this, start, end) as Frame[], this.loopCount); + } + + /** + * Encodes the GIF + * @param quality 1-100, scales the palette size from 8 colors (1) to 256 colors (90 and above) + */ + async encode(quality = 95): Promise { + const frames: codec.GIFFrame[] = []; + + for (const frame of this) { + if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames'); + + frames.push({ + x: frame.xOffset, + y: frame.yOffset, + width: frame.width, + height: frame.height, + // gifs store centiseconds; rounding keeps e.g. 16 ms (60 fps) from becoming 10 ms + delay: Math.round(frame.duration / 10), + dispose: frame.__disposal_id__, + pixels: new Uint8Array(frame.bitmap.buffer, frame.bitmap.byteOffset, frame.bitmap.byteLength) + }); + } + + return codec.encode({ width: this.width, height: this.height, loopCount: this.loopCount, frames }, quality); + } + + /** + * Decodes a GIF into fully composited frames + * @param input The binary data to decode + * @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame + */ + static async decode(input: BinaryData, onlyExtractFirstFrame = false): Promise { + const data = view(input); + if (!ImageType.isGIF(new DataView(data.buffer, data.byteOffset, data.byteLength))) throw new Error('Unsupported image type'); + + const decoded = codec.decode(data, onlyExtractFirstFrame); + const frames = codec.composite(decoded).map(({ pixels, delay }) => { + const frame = new Frame(decoded.width, decoded.height, 10 * delay, 0, 0, Frame.DISPOSAL_BACKGROUND); + frame.bitmap.set(pixels); + return frame; + }); + + return new GIF(frames, decoded.loopCount); + } + + /** + * Resizes the GIF to the given dimensions, scaling every frame and its offsets proportionally. + * Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio. + */ + resize(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + const originalWidth = this.width, originalHeight = this.height; + if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO) throw new Error('RESIZE_AUTO can only be used for either width or height, not for both'); + if (width === Image.RESIZE_AUTO) width = originalWidth / originalHeight * height; + if (height === Image.RESIZE_AUTO) height = originalHeight / originalWidth * width; + + const sx = width / originalWidth, sy = height / originalHeight; + for (const frame of this) { + // scale both edges so adjacent frames stay aligned + const left = Math.round(frame.xOffset * sx), top = Math.round(frame.yOffset * sy); + const right = Math.round((frame.xOffset + frame.width) * sx), bottom = Math.round((frame.yOffset + frame.height) * sy); + Image.prototype.resize.call(frame, Math.max(1, right - left), Math.max(1, bottom - top), mode); + frame.xOffset = left; + frame.yOffset = top; + } + + return this; + } +} diff --git a/src/image-type.ts b/src/image-type.ts new file mode 100644 index 0000000..0556329 --- /dev/null +++ b/src/image-type.ts @@ -0,0 +1,65 @@ +import { BinaryData, view as bytes } from './util/mem.ts'; + +/** Image formats recognized by {@link ImageType.getType} */ +export type ImageTypeName = 'png' | 'jpeg' | 'tiff' | 'gif'; + +const MAGIC_NUMBERS = { + PNG: 0x89504e47, + JPEG: 0xffd8ff, + TIFF_LE: 0x49492a00, + TIFF_BE: 0x4d4d002a, + BIGTIFF_LE: 0x49492b00, + BIGTIFF_BE: 0x4d4d002b, + GIF: 0x474946 +}; + +// any binary data as a DataView (DataViews are used as is) +function dataView(data: BinaryData): DataView { + if (data instanceof DataView) return data; + const u8 = bytes(data); + return new DataView(u8.buffer, u8.byteOffset, u8.byteLength); +} + +/** Detects image formats by their magic numbers */ +export class ImageType { + /** + * Gets an image's type from its magic number + * @param data The image binary to get the type of + * @returns The image type, or null if it isn't supported + */ + static getType(data: BinaryData): ImageTypeName | null { + const view = dataView(data); + + if (this.isPNG(view)) return 'png'; + if (this.isJPEG(view)) return 'jpeg'; + if (this.isTIFF(view)) return 'tiff'; + if (this.isGIF(view)) return 'gif'; + return null; + } + + /** Whether the data starts like a PNG */ + static isPNG(data: BinaryData): boolean { + const view = dataView(data); + return view.byteLength >= 4 && view.getUint32(0, false) === MAGIC_NUMBERS.PNG; + } + + /** Whether the data starts like a JPEG */ + static isJPEG(data: BinaryData): boolean { + const view = dataView(data); + return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.JPEG; + } + + /** Whether the data starts like a TIFF (or BigTIFF) */ + static isTIFF(data: BinaryData): boolean { + const view = dataView(data); + if (view.byteLength < 4) return false; + const magic = view.getUint32(0, false); + return magic === MAGIC_NUMBERS.TIFF_LE || magic === MAGIC_NUMBERS.TIFF_BE || magic === MAGIC_NUMBERS.BIGTIFF_LE || magic === MAGIC_NUMBERS.BIGTIFF_BE; + } + + /** Whether the data starts like a GIF */ + static isGIF(data: BinaryData): boolean { + const view = dataView(data); + return view.byteLength >= 4 && (view.getUint32(0, false) >>> 8) === MAGIC_NUMBERS.GIF; + } +} diff --git a/src/image.ts b/src/image.ts new file mode 100644 index 0000000..2340b7c --- /dev/null +++ b/src/image.ts @@ -0,0 +1,1873 @@ +import * as png from './codecs/png.ts'; +import * as jpeg from './codecs/jpeg.ts'; +import * as tiff from './codecs/tiff.ts'; +import * as quantize from './codecs/quantize.ts'; +import * as textRenderer from './render/text.ts'; +import * as svgRenderer from './render/svg.ts'; +import { BinaryData, view } from './util/mem.ts'; +import { ImageType } from './image-type.ts'; +import { TextLayout } from './text-layout.ts'; +import { VERSION } from './version.ts'; +import { resample, BOX, TRIANGLE, CUBIC } from './util/resample.ts'; +import { gaussianBlur, premultiply, unpremultiply } from './util/blur.ts'; + +/** A color as a 32-bit RGBA integer (`0xRRGGBBAA`) */ +export type Color = number; + +/** Called with a pixel's (0-based) coordinates, returns its color */ +export type ColorFunction = (x: number, y: number) => Color; + +/** Resampling filters for {@link Image.resize} and the other scaling functions, see {@link Image.RESIZE_BILINEAR} etc. */ +export type ResizeMode = 'RESIZE_NEAREST_NEIGHBOR' | 'RESIZE_BILINEAR' | 'RESIZE_BICUBIC' | 'RESIZE_AREA'; + +/** How {@link Image.renderSVG} scales: {@link Image.SVG_MODE_SCALE}, {@link Image.SVG_MODE_WIDTH} or {@link Image.SVG_MODE_HEIGHT} */ +export type SVGScaleMode = 1 | 2 | 3; + +/** Blend modes of {@link Image.composite}, named like CSS' mix-blend-mode values */ +export type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' + | 'hard-light' | 'soft-light' | 'difference' | 'exclusion' | 'add'; + +/** Options of {@link Image.composite} */ +export interface CompositeOptions { + /** Opacity of the source (0..1). Default: `1` */ + opacity?: number; + /** How the colors are mixed. Default: `'normal'` */ + mode?: BlendMode; +} + +/** Options of {@link Image.decode} */ +export interface DecodeOptions { + /** Whether to rotate/mirror JPEGs, TIFFs and PNGs as their orientation tag (Exif or XMP) says. Default: `true` */ + applyOrientation?: boolean; +} + +/** Text style options of {@link Image.renderText} */ +export interface TextStyle { + /** Width of an outline around the glyphs in pixels. Default: `0` */ + strokeWidth?: number; + /** Color of the outline. Default: `0x000000ff` */ + strokeColor?: Color; +} + +/** Options of {@link Image.cropCircle} */ +export interface CropCircleOptions { + /** Whether to use the larger dimension for the size. Default: `false` */ + max?: boolean; + /** Share of the radius (0..1) over which the edge fades out. Default: `0` */ + feathering?: number; +} + +/** Options of {@link Image.dominantColor} */ +export interface DominantColorOptions { + /** Whether to ignore dark colors (all channels below the threshold). Default: `true` */ + ignoreBlack?: boolean; + /** Whether to ignore light colors (all channels above 255 minus the threshold). Default: `true` */ + ignoreWhite?: boolean; + /** The black/white threshold (0-64, in steps of 4 channel values). Default: `15` */ + threshold?: number; +} + +/** Options of {@link Image.rotate} */ +export interface RotateOptions { + /** Whether to resize the image so it fits all pixels or just ignore outlying pixels. Default: `true` */ + resize?: boolean; +} + +/** Text metadata written into PNGs by {@link Image.encode} */ +export interface PNGMetadata { + /** The image's title */ + title?: string; + /** The image's author */ + author?: string; + /** The image's description */ + description?: string; + /** The image's copyright info */ + copyright?: string; + /** The image's creation time. Default: now */ + creationTime?: string | number | Date; + /** The software used to create the image. Default: `'github.com/matmen/ImageScript vX.X.X'`, `''` leaves it out */ + software?: string; + /** A disclaimer for the image */ + disclaimer?: string; + /** A warning for the image */ + warning?: string; + /** The image's source */ + source?: string; + /** A comment for the image */ + comment?: string; +} + +/** + * An RGBA image; provides the codecs and the manipulation functions. Methods that change the image modify it in place + * and return it, so calls can be chained + */ +export class Image { + /** @internal */ + protected __width__: number; + /** @internal */ + protected __height__: number; + /** @internal */ + protected __view__!: DataView; + /** @internal */ + protected __u32__!: Uint32Array; + /** @internal */ + protected __bitmap__!: Uint8ClampedArray; + + /** + * Creates a new image with the given dimensions + * @param width + * @param height + */ + constructor(width: number, height: number) { + width = ~~width; + height = ~~height; + + if (width < 1) + throw new RangeError('Image has to be at least 1 pixel wide'); + if (height < 1) + throw new RangeError('Image has to be at least 1 pixel high'); + + this.__width__ = width; + this.__height__ = height; + this.__set_bitmap__(new Uint8ClampedArray(width * height * 4)); + } + + /** + * The image's RGBA pixel data. It can be replaced by any 8-bit array with width * height * 4 bytes + * (it is used as is, without copying) + */ + get bitmap(): Uint8ClampedArray { + return this.__bitmap__; + } + + /** Replaces the pixel data, the array has to hold width * height * 4 bytes */ + set bitmap(bitmap: Uint8ClampedArray | Uint8Array) { + if (!(bitmap instanceof Uint8ClampedArray || bitmap instanceof Uint8Array)) + throw new TypeError('The bitmap has to be a Uint8ClampedArray or Uint8Array'); + if (bitmap.length !== this.width * this.height * 4) + throw new RangeError(`The bitmap has to have ${this.width * this.height * 4} bytes (width * height * 4), got ${bitmap.length}`); + + this.__set_bitmap__(bitmap instanceof Uint8ClampedArray ? bitmap : new Uint8ClampedArray(bitmap.buffer, bitmap.byteOffset, bitmap.length)); + } + + /** + * Sets the pixel data and the views on it + * @internal + */ + protected __set_bitmap__(bitmap: Uint8ClampedArray): void { + // 32-bit views need an aligned offset + if (bitmap.byteOffset % 4) bitmap = bitmap.slice(); + this.__bitmap__ = bitmap; + this.__view__ = new DataView(bitmap.buffer, bitmap.byteOffset, bitmap.byteLength); + this.__u32__ = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2); + } + + /** + * @private + */ + toString(): string { + return `Image<${this.width}x${this.height}>`; + } + + /** + * The images width + */ + get width(): number { + return this.__width__; + } + + /** + * The images height + */ + get height(): number { + return this.__height__; + } + + /** + * Yields an [x, y] array for every pixel in the image (0-based) + * @yields The coordinates of the pixel ([x, y]) + */ + *[Symbol.iterator](): Generator<[x: number, y: number], void, undefined> { + for (let y = 0; y < this.height; y++) + for (let x = 0; x < this.width; x++) + yield [x, y]; + } + + /** + * Yields an [x, y, color] array for every pixel in the image (0-based) + * @yields The coordinates and color of the pixel ([x, y, color]) + */ + *iterateWithColors(): Generator<[x: number, y: number, color: Color], void, undefined> { + const view = new DataView(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength); + let offset = 0; + for (let y = 0; y < this.height; y++) + for (let x = 0; x < this.width; x++, offset += 4) + yield [x, y, view.getUint32(offset, false)]; + } + + /** + * Converts RGBA components to an RGBA value, values are rounded and clamped to 0..255 + * @param r red (0..255) + * @param g green (0..255) + * @param b blue (0..255) + * @param a alpha (0..255) + * @returns RGBA value + */ + static rgbaToColor(r: number, g: number, b: number, a: number): Color { + return (channel(r) << 24 | channel(g) << 16 | channel(b) << 8 | channel(a)) >>> 0; + } + + /** + * Converts RGB components to an RGBA value (assuming alpha = 255) + * @param r red (0..255) + * @param g green (0..255) + * @param b blue (0..255) + * @returns RGBA value + */ + static rgbToColor(r: number, g: number, b: number): Color { + return Image.rgbaToColor(r, g, b, 0xff); + } + + /** + * Converts HSLA colors to RGBA colors + * @param h hue (0..1) + * @param s saturation (0..1) + * @param l lightness (0..1) + * @param a opacity (0..1) + * @returns color + */ + static hslaToColor(h: number, s: number, l: number, a: number): Color { + h = wrapHue(h); + s = Math.min(1, Math.max(0, s)); + l = Math.min(1, Math.max(0, l)); + a = Math.min(1, Math.max(0, a)); + + let r: number, g: number, b: number; + + if (s === 0) { + r = g = b = l; + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + return Image.rgbaToColor(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), Math.round(a * 255)); + } + + /** + * Converts HSL colors to RGBA colors (assuming an opacity of 255) + * @param h hue (0..1) + * @param s saturation (0..1) + * @param l lightness (0..1) + * @returns color + */ + static hslToColor(h: number, s: number, l: number): Color { + return Image.hslaToColor(h, s, l, 1); + } + + /** + * Converts an RGBA value to an array of HSLA values + * @param r {number} (0..255) + * @param g {number} (0..255) + * @param b {number} (0..255) + * @param a {number} (0..255) + * @returns The HSLA values ([H, S, L, A]) + */ + static rgbaToHSLA(r: number, g: number, b: number, a: number): [h: number, s: number, l: number, a: number] { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h = 0, s: number; + const l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + + h /= 6; + } + + return [h, s, l, a / 255]; + } + + /** + * Converts a color value to an array of RGBA values + * @param color The color value to convert + * @returns The RGBA values ([R, G, B, A]) + */ + static colorToRGBA(color: Color): [r: number, g: number, b: number, a: number] { + return [(color >> 24) & 0xff, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff]; + } + + /** + * Converts a color value to an array of RGB values (ignoring the colors alpha) + * @param color The color value to convert + * @returns The RGB values ([R, G, B]) + */ + static colorToRGB(color: Color): [r: number, g: number, b: number] { + const [r, g, b] = Image.colorToRGBA(color); + return [r, g, b]; + } + + /** + * Gets the pixel color at the specified position (0-based) + * @param x + * @param y + * @returns The color value + */ + getPixelAt(x: number, y: number): Color { + x = ~~x; + y = ~~y; + this.__check_boundaries__(x, y); + return this.__view__.getUint32((y * this.width + x) * 4, false); + } + + /** + * Gets a view of the RGBA components of the pixel at the specified position (0-based) + * @param x + * @param y + * @returns The RGBA value + */ + getRGBAAt(x: number, y: number): Uint8ClampedArray { + x = ~~x; + y = ~~y; + this.__check_boundaries__(x, y); + const idx = (y * this.width + x) * 4; + return this.bitmap.subarray(idx, idx + 4); + } + + /** + * Sets the pixel color for the specified position (0-based) + * @param x + * @param y + * @param pixelColor + */ + setPixelAt(x: number, y: number, pixelColor: Color): this { + x = ~~x; + y = ~~y; + this.__check_boundaries__(x, y); + this.__set_pixel__(x, y, pixelColor); + return this; + } + + /** + * @private + * @param x + * @param y + * @param pixelColor + */ + /** @internal */ + protected __set_pixel__(x: number, y: number, pixelColor: Color): void { + this.__view__.setUint32((y * this.width + x) * 4, pixelColor, false); + } + + /** + * @private + * @param x + * @param y + */ + /** @internal */ + protected __check_boundaries__(x: number, y: number): void { + if (isNaN(x)) throw new TypeError(`Invalid pixel coordinates (x=${x})`); + if (isNaN(y)) throw new TypeError(`Invalid pixel coordinates (y=${y})`); + if (x < 0) + throw new RangeError(`${Image.__out_of_bounds__} (x=${x})<0`); + if (x >= this.width) + throw new RangeError(`${Image.__out_of_bounds__} (x=${x})>=(width=${this.width})`); + if (y < 0) + throw new RangeError(`${Image.__out_of_bounds__} (y=${y})<0`); + if (y >= this.height) + throw new RangeError(`${Image.__out_of_bounds__} (y=${y})>=(height=${this.height})`); + } + + /** + * @private + */ + /** @internal */ + protected static get __out_of_bounds__(): string { + return 'Tried referencing a pixel outside of the images boundaries:'; + } + + /** + * @callback colorFunction + * @param x 0-based x coordinate + * @param y 0-based y coordinate + * @returns pixel color + */ + + /** + * Fills the image data with the supplied color + * @param color + */ + fill(color: Color | ColorFunction): this { + const view = new DataView(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength); + + if (typeof color === 'function') { + let offset = 0; + for (let y = 0; y < this.height; y++) + for (let x = 0; x < this.width; x++, offset += 4) + view.setUint32(offset, color(x, y), false); + } else { + if (!this.bitmap.length) return this; + view.setUint32(0, color, false); + const u32 = new Uint32Array(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength / 4); + u32.fill(u32[0]); + } + + return this; + } + + /** + * Clones the current image + */ + clone(): Image { + const image = new Image(this.width, this.height); + image.bitmap.set(this.bitmap); + return image; + } + + /** + * Use {@link https://en.wikipedia.org/wiki/Image_scaling#Nearest-neighbor_interpolation Nearest-neighbor} resizing. + */ + static get RESIZE_NEAREST_NEIGHBOR(): 'RESIZE_NEAREST_NEIGHBOR' { + return 'RESIZE_NEAREST_NEIGHBOR'; + } + + /** + * Bilinear resizing (the default). When shrinking, the filter widens with the scale factor, so every source pixel contributes + */ + static get RESIZE_BILINEAR(): 'RESIZE_BILINEAR' { + return 'RESIZE_BILINEAR'; + } + + /** + * Bicubic (Catmull-Rom) resizing, sharper than bilinear, especially when enlarging + */ + static get RESIZE_BICUBIC(): 'RESIZE_BICUBIC' { + return 'RESIZE_BICUBIC'; + } + + /** + * Area averaging (box filter): every output pixel is the average of the source pixels it covers. Meant for shrinking + */ + static get RESIZE_AREA(): 'RESIZE_AREA' { + return 'RESIZE_AREA'; + } + + /** + * Used for automatically preserving an images aspect ratio when resizing. + */ + static get RESIZE_AUTO(): -1 { + return -1; + } + + /** + * Resizes the image by the given factor + * @param factor The factor to resize the image with + * @param [mode=Image.RESIZE_BILINEAR] The resizing mode to use + */ + scale(factor: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + const image = this.__scale__(factor, mode); + return this.__apply__(image); + } + + /** @private */ + /** @internal */ + protected __scale__(factor: number, mode: ResizeMode = Image.RESIZE_BILINEAR): Image { + if (factor === 1) return this; + return this.__resize__(this.width * factor, this.height * factor, mode); + } + + /** + * Resizes the image to the given dimensions. + * Use {@link Image.RESIZE_AUTO} as either width or height to automatically preserve the aspect ratio. + * @param width The new width + * @param height The new height + * @param [mode=Image.RESIZE_BILINEAR] The resizing mode to use + * @returns The resized image + */ + resize(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + const image = this.__resize__(width, height, mode); + return this.__apply__(image); + } + + /** + * Resizes the image so it is contained in the given bounding box. + * Can return an image with one axis smaller than the given bounding box. + * @param width The width of the bounding box + * @param height The height of the bounding box + * @param [mode=Image.RESIZE_BILINEAR] The resizing mode to use + * @returns The resized image + */ + contain(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + width = Math.floor(width); + height = Math.floor(height); + // round instead of flooring the scaled size, so the limiting axis matches the box exactly + const scale = Math.min(width / this.width, height / this.height); + return this.resize(Math.max(1, Math.min(width, Math.round(this.width * scale))), Math.max(1, Math.min(height, Math.round(this.height * scale))), mode); + } + + /** + * Resizes the image so it is contained in the given bounding box, placing it in the center of the given bounding box. + * Always returns the exact dimensions of the bounding box. + * @param width The width of the bounding box + * @param height The height of the bounding box + * @param [mode=Image.RESIZE_BILINEAR] The resizing mode to use + * @returns The resized image + */ + fit(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + const result = new Image(width, height); + this.contain(width, height, mode); + result.composite(this, (width - this.width) / 2, (height - this.height) / 2); + return this.__apply__(result); + } + + /** + * Resizes the image so it covers the given bounding box, cropping the overflowing edges. + * Always returns the exact dimensions of the bounding box. + * @param width The width of the bounding box + * @param height The height of the bounding box + * @param [mode=Image.RESIZE_BILINEAR] The resizing mode to use + * @returns The resized image + */ + cover(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): this { + width = Math.floor(width); + height = Math.floor(height); + const scale = Math.max(width / this.width, height / this.height); + this.resize(Math.max(width, Math.round(this.width * scale)), Math.max(height, Math.round(this.height * scale)), mode); + return this.crop((this.width - width) / 2, (this.height - height) / 2, width, height); + } + + /** @private */ + /** @internal */ + protected __resize__(width: number, height: number, mode: ResizeMode = Image.RESIZE_BILINEAR): Image { + if (width === Image.RESIZE_AUTO && height === Image.RESIZE_AUTO) throw new Error('RESIZE_AUTO can only be used for either width or height, not for both'); + else if (width === Image.RESIZE_AUTO) width = Math.max(1, Math.round(this.width / this.height * Math.floor(height))); + else if (height === Image.RESIZE_AUTO) height = Math.max(1, Math.round(this.height / this.width * Math.floor(width))); + + width = Math.floor(width); + height = Math.floor(height); + if (width < 1) + throw new RangeError('Image has to be at least 1 pixel wide'); + if (height < 1) + throw new RangeError('Image has to be at least 1 pixel high'); + + if (mode === Image.RESIZE_NEAREST_NEIGHBOR) return this.__resize_nearest_neighbor__(width, height); + + const filter = mode === Image.RESIZE_BILINEAR ? TRIANGLE : mode === Image.RESIZE_BICUBIC ? CUBIC : mode === Image.RESIZE_AREA ? BOX : null; + if (!filter) throw new Error('Invalid resize mode'); + + const image = new Image(width, height); + resample(this.bitmap, this.width, this.height, width, height, filter, image.bitmap); + return image; + } + + /** + * @private + * @param width The new width + * @param height The new height + */ + /** @internal */ + protected __resize_nearest_neighbor__(width: number, height: number): Image { + const image = new Image(width, height); + const src = this.__u32__; + const dst = image.__u32__; + + // sample at the centers of the output pixels + for (let y = 0; y < height; y++) { + const row = this.width * ((y + 0.5) * (this.height / height) | 0); + for (let x = 0; x < width; x++) + dst[y * width + x] = src[row + ((x + 0.5) * (this.width / width) | 0)]; + } + + return image; + } + + /** + * Crops an image to the specified dimensions. + * The region is clipped to the image, so the result can be smaller than the requested size + * @param x The x offset (0-based) + * @param y The y offset (0-based) + * @param width The new images width + * @param height The new images height + */ + crop(x: number, y: number, width: number, height: number): this { + x = ~~x; + y = ~~y; + width = ~~width; + height = ~~height; + + const left = Math.max(0, x), top = Math.max(0, y); + const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height); + if (right <= left || bottom <= top) + throw new RangeError('Crop region does not overlap the image'); + + return this.__apply__(this.__crop__(left, top, right - left, bottom - top)); + } + + /** + * @param x + * @param y + * @param width + * @param height + * @private + */ + /** @internal */ + protected __crop__(x: number, y: number, width: number, height: number): Image { + x = ~~x; + y = ~~y; + + const image = new Image(width, height); + + for (let tY = 0; tY < height; tY++) { + const idx = (tY + y) * this.width + x; + image.__u32__.set(this.__u32__.subarray(idx, idx + width), tY * width); + } + + return image; + } + + /** + * Draws a box at the specified coordinates (0-based), clipped to the image + * @param x The x offset + * @param y The y offset + * @param width The box width + * @param height The box height + * @param color The color to fill the box in with, a function gets coordinates relative to the box + */ + drawBox(x: number, y: number, width: number, height: number, color: Color | ColorFunction): this { + x = ~~x; + y = ~~y; + width = ~~width; + height = ~~height; + + if (typeof color !== 'function') return this.__fast_box__(x, y, width, height, color); + + for (let tY = Math.max(0, -y); tY < height && tY + y < this.height; tY++) + for (let tX = Math.max(0, -x); tX < width && tX + x < this.width; tX++) + this.__set_pixel__(tX + x, tY + y, color(tX, tY)); + + return this; + } + + /** + * @private + * @param x + * @param y + * @param width + * @param height + * @param color + */ + /** @internal */ + protected __fast_box__(x: number, y: number, width: number, height: number, color: Color): this { + const left = Math.max(0, x), top = Math.max(0, y); + const right = Math.min(this.width, x + width), bottom = Math.min(this.height, y + height); + if (right <= left || bottom <= top) return this; + + // fill the first row, then copy it to the others + for (let xPos = left; xPos < right; xPos++) + this.__view__.setUint32(4 * (xPos + top * this.width), color); + + const start = 4 * (left + top * this.width); + const end = 4 * (right + top * this.width); + for (let row = top + 1; row < bottom; row++) + this.bitmap.copyWithin(4 * (left + row * this.width), start, end); + + return this; + } + + /** + * Draws an antialiased circle around the center of the specified pixel (0-based) + * @param x The center x position + * @param y The center y position + * @param radius The circles radius in pixels + * @param color The color, a function gets coordinates relative to the circle's bounding box + */ + drawCircle(x: number, y: number, radius: number, color: Color | ColorFunction): this { + x = ~~x; + y = ~~y; + // the circle is centered on the pixel's center, its edge is antialiased + const cx = x + 0.5, cy = y + 0.5; + + for (let py = Math.max(0, Math.floor(cy - radius)); py < Math.min(this.height, Math.ceil(cy + radius)); py++) { + for (let px = Math.max(0, Math.floor(cx - radius)); px < Math.min(this.width, Math.ceil(cx + radius)); px++) { + const coverage = circleCoverage(px, py, cx, cy, radius); + if (coverage <= 0) continue; + + const value = typeof color === 'function' ? color(px - x + radius, py - y + radius) : color; + if (coverage >= 1) this.__set_pixel__(px, py, value); + else this.__blend_pixel__(px, py, value, coverage); + } + } + + return this; + } + + /** + * Moves a pixel towards a color by the given amount (with premultiplied alpha) + * @internal + */ + protected __blend_pixel__(x: number, y: number, color: Color, amount: number): void { + const i = (y * this.width + x) * 4, b = this.bitmap; + const sa = (color & 0xff) / 255 * amount, da = b[i + 3] / 255 * (1 - amount); + const a = sa + da; + if (a <= 0) { + b[i] = b[i + 1] = b[i + 2] = b[i + 3] = 0; + return; + } + + b[i] = ((color >>> 24) * sa + b[i] * da) / a; + b[i + 1] = ((color >> 16 & 0xff) * sa + b[i + 1] * da) / a; + b[i + 2] = ((color >> 8 & 0xff) * sa + b[i + 2] * da) / a; + b[i + 3] = a * 255; + } + + /** + * Crops the image into a circle with an antialiased edge + * @param options See {@link CropCircleOptions} + */ + cropCircle(options?: CropCircleOptions): this; + /** + * Crops the image into a circle with an antialiased edge + * @param max Whether to use the larger dimension for the size. Default: `false` + * @param feathering Share of the radius (0..1) over which the edge fades out. Default: `0` + */ + cropCircle(max?: boolean, feathering?: number): this; + cropCircle(maxOrOptions: boolean | CropCircleOptions = false, featheringArgument = 0): this { + const { max = false, feathering = 0 } = typeof maxOrOptions === 'object' && maxOrOptions !== null + ? maxOrOptions : { max: maxOrOptions, feathering: featheringArgument }; + const radius = (max ? Math.max : Math.min)(this.width, this.height) / 2; + const cx = this.width / 2, cy = this.height / 2; + // width of the soft edge, at least the one pixel of antialiasing + const band = Math.min(1, Math.max(0, feathering)) * radius; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const alpha = 4 * (y * this.width + x) + 3; + const dx = x + 0.5 - cx, dy = y + 0.5 - cy; + // pixels well inside keep their alpha + if (dx * dx + dy * dy <= (radius - Math.max(band, 1)) ** 2) continue; + + const factor = band > 1 + ? Math.min(1, Math.max(0, (radius - Math.hypot(dx, dy)) / band)) + : circleCoverage(x, y, cx, cy, radius); + if (factor < 1) this.bitmap[alpha] *= factor; + } + } + + return this; + } + + /** + * Sets the images opacity + * @param opacity The opacity to apply (0..1) + * @param absolute Whether to scale the current opacity (false) or just set the new opacity (true) + */ + opacity(opacity: number, absolute = false): this { + if (isNaN(opacity) || opacity < 0) + throw new RangeError('Invalid opacity value'); + + this.__set_channel_value__(opacity, absolute, 3); + + return this; + } + + /** + * Scales or sets the red channel + * @param factor The factor to multiply the channel with (>= 0, results are clamped), or the value (0..1) to set it to + * @param absolute Whether to scale the current value (false) or set every pixel's red channel to `factor` * 255 (true) + */ + red(factor: number, absolute = false): this { + if (isNaN(factor) || factor < 0) + throw new RangeError('Invalid red factor'); + + this.__set_channel_value__(factor, absolute, 0); + + return this; + } + + /** + * Scales or sets the green channel + * @param factor The factor to multiply the channel with (>= 0, results are clamped), or the value (0..1) to set it to + * @param absolute Whether to scale the current value (false) or set every pixel's green channel to `factor` * 255 (true) + */ + green(factor: number, absolute = false): this { + if (isNaN(factor) || factor < 0) + throw new RangeError('Invalid green factor'); + + this.__set_channel_value__(factor, absolute, 1); + + return this; + } + + /** + * Scales or sets the blue channel + * @param factor The factor to multiply the channel with (>= 0, results are clamped), or the value (0..1) to set it to + * @param absolute Whether to scale the current value (false) or set every pixel's blue channel to `factor` * 255 (true) + */ + blue(factor: number, absolute = false): this { + if (isNaN(factor) || factor < 0) + throw new RangeError('Invalid blue factor'); + + this.__set_channel_value__(factor, absolute, 2); + + return this; + } + + /** + * @private + * @param value + * @param absolute + * @param offset + */ + /** @internal */ + protected __set_channel_value__(value: number, absolute: boolean, offset: number): void { + for (let i = offset; i < this.bitmap.length; i += 4) + this.bitmap[i] = value * (absolute ? 255 : this.bitmap[i]); + } + + /** + * Sets the brightness of the image + * @param value The lightness to apply (0..1) + * @param absolute Whether to scale the current lightness (false) or just set the new lightness (true) + */ + lightness(value: number, absolute = false): this { + if (isNaN(value) || value < 0) + throw new RangeError('Invalid lightness value'); + + return this.__map_hsla__(c => { c.l = value * (absolute ? 1 : c.l); }); + } + + /** + * Sets the saturation of the image + * @param value The saturation to apply (0..1) + * @param absolute Whether to scale the current saturation (false) or just set the new saturation (true) + */ + saturation(value: number, absolute = false): this { + if (isNaN(value) || value < 0) + throw new RangeError('Invalid saturation value'); + + return this.__map_hsla__(c => { c.s = value * (absolute ? 1 : c.s); }); + } + + /** + * Composites (overlays) the source onto this image at the specified coordinates + * @param source The image to place + * @param [x=0] The x position to place the image at + * @param [y=0] The y position to place the image at + * @param options Opacity and blend mode, see {@link CompositeOptions} + */ + composite(source: Image, x = 0, y = 0, options: CompositeOptions = {}): this { + const { opacity = 1, mode = 'normal' } = options; + x |= 0; + y |= 0; + + if (isNaN(opacity) || opacity < 0 || opacity > 1) + throw new RangeError('Invalid composite opacity'); + const blend = BLEND_MODES[mode]; + if (!blend || !Object.prototype.hasOwnProperty.call(BLEND_MODES, mode)) + throw new RangeError(`Invalid blend mode: ${mode}`); + if (opacity !== 1 || mode !== 'normal') return this.__blend__(source, x, y, opacity, blend); + + const left = Math.max(0, x), top = Math.max(0, y); + const right = Math.min(this.width, x + source.width), bottom = Math.min(this.height, y + source.height); + const dst = this.bitmap, src = source.bitmap; + // whole pixels are copied through 32-bit views + const dst32 = new Uint32Array(dst.buffer, dst.byteOffset, dst.length >> 2); + const src32 = new Uint32Array(src.buffer, src.byteOffset, src.length >> 2); + + for (let row = top; row < bottom; row++) { + for (let column = left; column < right; column++) { + const sp = (row - y) * source.width + column - x, s = 4 * sp; + const sa = src[s + 3]; + if (sa === 0) continue; + + const dp = row * this.width + column, d = 4 * dp; + const da = dst[d + 3]; + if (sa === 255 || da === 0) { + dst32[dp] = src32[sp]; + continue; + } + + // source over with straight alpha, the clamped bitmap rounds on assignment + const fa = sa / 255, ba = da / 255 * (1 - fa); + const a = fa + ba; + dst[d] = (src[s] * fa + dst[d] * ba) / a; + dst[d + 1] = (src[s + 1] * fa + dst[d + 1] * ba) / a; + dst[d + 2] = (src[s + 2] * fa + dst[d + 2] * ba) / a; + dst[d + 3] = a * 255; + } + } + + return this; + } + + /** + * Composites with opacity and a blend mode (the W3C compositing model: the blended color is drawn source-over) + * @internal + */ + protected __blend__(source: Image, x: number, y: number, opacity: number, blend: (backdrop: number, source: number) => number): this { + const left = Math.max(0, x), top = Math.max(0, y); + const right = Math.min(this.width, x + source.width), bottom = Math.min(this.height, y + source.height); + const dst = this.bitmap, src = source.bitmap; + + for (let row = top; row < bottom; row++) { + for (let column = left; column < right; column++) { + const s = 4 * ((row - y) * source.width + column - x); + const as = src[s + 3] / 255 * opacity; + if (as <= 0) continue; + + const d = 4 * (row * this.width + column); + const ab = dst[d + 3] / 255; + const ao = as + ab * (1 - as); + + for (let c = 0; c < 3; c++) { + const cs = src[s + c] / 255, cb = dst[d + c] / 255; + // where the backdrop is transparent the source color is used unblended + const cr = (1 - ab) * cs + ab * blend(cb, cs); + // the clamped bitmap rounds on assignment + dst[d + c] = (as * cr + ab * (1 - as) * cb) / ao * 255; + } + dst[d + 3] = ao * 255; + } + } + + return this; + } + + /** + * Inverts the images colors + */ + invert(): this { + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) { + bitmap[i] = 255 - bitmap[i]; + bitmap[i + 1] = 255 - bitmap[i + 1]; + bitmap[i + 2] = 255 - bitmap[i + 2]; + } + + return this; + } + + /** + * Inverts the images lightness (HSL), keeping hue and saturation: dark colors become light and vice versa + */ + invertLightness(): this { + return this.__map_hsla__(c => { c.l = 1 - c.l; }); + } + + /** + * Inverts the images value (HSV), keeping hue and saturation: the brightest channel `v` becomes `255 - v` + * and the others scale along, black becomes white + */ + invertValue(): this { + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) { + const max = Math.max(bitmap[i], bitmap[i + 1], bitmap[i + 2]); + if (max === 0) { + bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = 255; + continue; + } + + // the clamped bitmap rounds on assignment + const factor = (255 - max) / max; + bitmap[i] *= factor; + bitmap[i + 1] *= factor; + bitmap[i + 2] *= factor; + } + + return this; + } + + /** + * Shifts the images hue + * @param degrees How many degrees to shift the hue by + */ + hueShift(degrees: number): this { + return this.__map_hsla__(c => { c.h += degrees / 360; }); + } + + /** + * Converts every pixel to HSLA, lets `map` adjust the components in place and converts back + * (the same math as {@link Image.rgbaToHSLA} and {@link Image.hslaToColor}, without allocating per pixel) + * @internal + */ + protected __map_hsla__(map: (hsla: HSLA) => void): this { + const bitmap = this.bitmap; + const hsla: HSLA = { h: 0, s: 0, l: 0, a: 0 }; + const pixels = new Uint32Array(bitmap.buffer, bitmap.byteOffset, bitmap.length >> 2); + + // results of recently seen colors (direct mapped by a hash of the color), images often repeat colors + const keys = new Uint32Array(4096), values = new Uint32Array(4096); + keys[0] = 1; // so the empty slot 0 can't match the color 0 + + for (let i = 0; i < bitmap.length; i += 4) { + const color = pixels[i >> 2]; + const slot = Math.imul(color, 0x9e3779b1) >>> 20; + if (keys[slot] === color) { + pixels[i >> 2] = values[slot]; + continue; + } + + const r = bitmap[i] / 255, g = bitmap[i + 1] / 255, b = bitmap[i + 2] / 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + const l = (max + min) / 2; + let h = 0, s = 0; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + if (max === r) h = (g - b) / d + (g < b ? 6 : 0); + else if (max === g) h = (b - r) / d + 2; + else h = (r - g) / d + 4; + h /= 6; + } + + hsla.h = h; + hsla.s = s; + hsla.l = l; + hsla.a = bitmap[i + 3] / 255; + map(hsla); + + // see hslaToColor + const nh = wrapHue(hsla.h); + const ns = Math.min(1, Math.max(0, hsla.s)); + const nl = Math.min(1, Math.max(0, hsla.l)); + const na = Math.min(1, Math.max(0, hsla.a)); + + if (ns === 0) bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = Math.round(nl * 255); + else { + const q = nl < 0.5 ? nl * (1 + ns) : nl + ns - nl * ns; + const p = 2 * nl - q; + bitmap[i] = Math.round(hue2rgb(p, q, nh + 1 / 3) * 255); + bitmap[i + 1] = Math.round(hue2rgb(p, q, nh) * 255); + bitmap[i + 2] = Math.round(hue2rgb(p, q, nh - 1 / 3) * 255); + } + bitmap[i + 3] = Math.round(na * 255); + + keys[slot] = color; + values[slot] = pixels[i >> 2]; + } + + return this; + } + + /** + * Blurs the image with a gaussian blur (transparent pixels don't bleed their color) + * @param radius The blur radius: the gaussian's standard deviation in pixels, like CSS' blur() + */ + blur(radius: number): this { + if (isNaN(radius) || radius < 0) + throw new RangeError('Invalid blur radius'); + + const pixels = premultiply(this.bitmap); + gaussianBlur(pixels, this.width, this.height, radius); + unpremultiply(pixels, this.bitmap); + return this; + } + + /** + * Sharpens the image with an unsharp mask: the difference to a blurred copy is amplified + * @param [amount=1] How strongly to sharpen (0 does nothing) + * @param [radius=1] The radius of the blur the image is compared with (larger values sharpen coarser details) + */ + sharpen(amount = 1, radius = 1): this { + if (isNaN(amount) || amount < 0) + throw new RangeError('Invalid sharpen amount'); + if (isNaN(radius) || radius <= 0) + throw new RangeError('Invalid sharpen radius'); + + const blurred = premultiply(this.bitmap); + gaussianBlur(blurred, this.width, this.height, radius); + + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) { + const a = blurred[i + 3]; + if (a < 0.5 || bitmap[i + 3] === 0) continue; + + // the clamped bitmap rounds and clamps + const f = 255 / a; + bitmap[i] += amount * (bitmap[i] - blurred[i] * f); + bitmap[i + 1] += amount * (bitmap[i + 1] - blurred[i + 1] * f); + bitmap[i + 2] += amount * (bitmap[i + 2] - blurred[i + 2] * f); + } + + return this; + } + + /** + * Multiplies the color channels, e.g. 1.2 makes the image 20% brighter + * @param factor The factor (>= 0, 1 keeps the image) + */ + brightness(factor: number): this { + if (isNaN(factor) || factor < 0) + throw new RangeError('Invalid brightness factor'); + return this.__map_channels__(v => v * factor); + } + + /** + * Scales the color channels' distance from mid grey, e.g. 1.5 increases the contrast, 0.5 reduces it + * @param factor The factor (>= 0, 1 keeps the image, 0 makes it grey) + */ + contrast(factor: number): this { + if (isNaN(factor) || factor < 0) + throw new RangeError('Invalid contrast factor'); + return this.__map_channels__(v => (v - 127.5) * factor + 127.5); + } + + /** + * Applies a gamma correction, values above 1 brighten the dark tones, values below 1 darken them + * @param gamma The gamma (> 0, 1 keeps the image) + */ + gamma(gamma: number): this { + if (isNaN(gamma) || gamma <= 0) + throw new RangeError('Invalid gamma'); + return this.__map_channels__(v => 255 * (v / 255) ** (1 / gamma)); + } + + /** + * Converts the image to greyscale by its luma (ITU-R 601, like Pillow), keeping the alpha + */ + greyscale(): this { + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) + bitmap[i] = bitmap[i + 1] = bitmap[i + 2] = bitmap[i] * 19595 + bitmap[i + 1] * 38470 + bitmap[i + 2] * 7471 + 0x8000 >> 16; + return this; + } + + /** + * Applies a sepia tone + */ + sepia(): this { + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) { + const r = bitmap[i], g = bitmap[i + 1], b = bitmap[i + 2]; + bitmap[i] = 0.393 * r + 0.769 * g + 0.189 * b; + bitmap[i + 1] = 0.349 * r + 0.686 * g + 0.168 * b; + bitmap[i + 2] = 0.272 * r + 0.534 * g + 0.131 * b; + } + return this; + } + + /** + * Maps every color channel value through a function (evaluated once per value 0..255), alpha is kept + * @internal + */ + protected __map_channels__(map: (value: number) => number): this { + const lut = new Uint8ClampedArray(256); + for (let v = 0; v < 256; v++) lut[v] = map(v); + + const bitmap = this.bitmap; + for (let i = 0; i < bitmap.length; i += 4) { + bitmap[i] = lut[bitmap[i]]; + bitmap[i + 1] = lut[bitmap[i + 1]]; + bitmap[i + 2] = lut[bitmap[i + 2]]; + } + + return this; + } + + /** + * Gets the average color of the image, weighting each pixel by its opacity + * @returns The average color (fully opaque), black for fully transparent images + */ + averageColor(): Color { + let r = 0, g = 0, b = 0, weight = 0; + for (let i = 0; i < this.bitmap.length; i += 4) { + const a = this.bitmap[i + 3]; + r += this.bitmap[i] * a; + g += this.bitmap[i + 1] * a; + b += this.bitmap[i + 2] * a; + weight += a; + } + + if (!weight) return Image.rgbaToColor(0, 0, 0, 0xff); + return Image.rgbaToColor(Math.round(r / weight), Math.round(g / weight), Math.round(b / weight), 0xff); + } + + /** + * Gets the image's dominant color: the colors are grouped into a few clusters (median cut and k-means), + * the average color of the largest one is returned + * @param options Which colors to ignore, see {@link DominantColorOptions} + * @return The image's dominant color (fully opaque); mostly transparent pixels are ignored. + * If every pixel is ignored as black or white, the filter is dropped + */ + dominantColor(options?: DominantColorOptions): Color; + /** + * Gets the image's dominant color, see {@link Image.dominantColor | the variant with options} + * @param ignoreBlack Whether to ignore dark colors (all channels below the threshold). Default: `true` + * @param ignoreWhite Whether to ignore light colors (all channels above 255 minus the threshold). Default: `true` + * @param bwThreshold The black/white threshold (0-64, in steps of 4 channel values). Default: `15` + * @return The image's dominant color (fully opaque) + */ + dominantColor(ignoreBlack?: boolean, ignoreWhite?: boolean, bwThreshold?: number): Color; + dominantColor(ignoreBlackOrOptions: boolean | DominantColorOptions = true, ignoreWhiteArgument = true, thresholdArgument = 0xf): Color { + const { ignoreBlack = true, ignoreWhite = true, threshold: bwThreshold = 0xf } = typeof ignoreBlackOrOptions === 'object' && ignoreBlackOrOptions !== null + ? ignoreBlackOrOptions : { ignoreBlack: ignoreBlackOrOptions, ignoreWhite: ignoreWhiteArgument, threshold: thresholdArgument }; + const threshold = Math.min(64, Math.max(0, bwThreshold)) * 4; + const pixels = this.__bytes__(); + + const color = quantize.dominantColor(pixels, (r, g, b) => + !(ignoreBlack && Math.max(r, g, b) < threshold) && !(ignoreWhite && Math.min(r, g, b) > 255 - threshold)) + || quantize.dominantColor(pixels, () => true); + + return color ? Image.rgbToColor(...color) : Image.rgbaToColor(0, 0, 0, 0xff); + } + + /** + * Rotates the image counterclockwise by the given amount of degrees + * @param angle The angle to rotate the image for (in degrees) + * @param options See {@link RotateOptions} + */ + rotate(angle: number, options?: RotateOptions): this; + /** + * Rotates the image counterclockwise by the given amount of degrees + * @param angle The angle to rotate the image for (in degrees) + * @param resize Whether to resize the image so it fits all pixels or just ignore outlying pixels. Default: `true` + */ + rotate(angle: number, resize?: boolean): this; + rotate(angle: number, resizeOrOptions: boolean | RotateOptions = true): this { + const { resize = true } = typeof resizeOrOptions === 'object' && resizeOrOptions !== null ? resizeOrOptions : { resize: resizeOrOptions }; + angle = ((angle % 360) + 360) % 360; + if (angle === 0) return this; + + const {width, height} = this; + const src = this.__u32__; + + // right angles are exact pixel permutations + if (angle === 90 || angle === 180 || angle === 270) { + const out = new Image(angle === 180 ? width : height, angle === 180 ? height : width); + const dst = out.__u32__; + + for (let y = 0; y < height; y++) + for (let x = 0; x < width; x++) { + const i = src[y * width + x]; + if (angle === 90) dst[(width - 1 - x) * height + y] = i; + else if (angle === 180) dst[(height - 1 - y) * width + width - 1 - x] = i; + else dst[x * height + height - 1 - y] = i; + } + + return this.__apply__(out); + } + + const rad = angle * Math.PI / 180; + const sin = Math.sin(rad), cos = Math.cos(rad); + const outWidth = resize ? Math.round(Math.abs(width * cos) + Math.abs(height * sin)) : width; + const outHeight = resize ? Math.round(Math.abs(width * sin) + Math.abs(height * cos)) : height; + const out = new Image(outWidth, outHeight); + const s8 = this.bitmap, d8 = out.bitmap; + + // bilinear sampling of the counterclockwise rotated source, with premultiplied alpha + for (let oy = 0; oy < outHeight; oy++) { + for (let ox = 0; ox < outWidth; ox++) { + const dx = ox + 0.5 - outWidth / 2, dy = oy + 0.5 - outHeight / 2; + const sx = dx * cos - dy * sin + width / 2 - 0.5; + const sy = dx * sin + dy * cos + height / 2 - 0.5; + const x0 = Math.floor(sx), y0 = Math.floor(sy); + if (x0 < -1 || y0 < -1 || x0 >= width || y0 >= height) continue; + + const fx = sx - x0, fy = sy - y0; + const w00 = (1 - fx) * (1 - fy), w10 = fx * (1 - fy), w01 = (1 - fx) * fy, w11 = fx * fy; + let r = 0, g = 0, b = 0, a = 0; + + // taps outside the source are skipped (zero weight taps add nothing either way) + const left = x0 >= 0, right = x0 + 1 < width, top = y0 >= 0, bottom = y0 + 1 < height; + let i = 4 * (y0 * width + x0); + if (top && left) { + const wa = w00 * s8[i + 3]; + r += wa * s8[i]; + g += wa * s8[i + 1]; + b += wa * s8[i + 2]; + a += wa; + } + if (top && right) { + const wa = w10 * s8[i + 7]; + r += wa * s8[i + 4]; + g += wa * s8[i + 5]; + b += wa * s8[i + 6]; + a += wa; + } + i += 4 * width; + if (bottom && left) { + const wa = w01 * s8[i + 3]; + r += wa * s8[i]; + g += wa * s8[i + 1]; + b += wa * s8[i + 2]; + a += wa; + } + if (bottom && right) { + const wa = w11 * s8[i + 7]; + r += wa * s8[i + 4]; + g += wa * s8[i + 5]; + b += wa * s8[i + 6]; + a += wa; + } + + if (a <= 0) continue; + // the clamped bitmap rounds on assignment + const o = 4 * (oy * outWidth + ox); + d8[o] = r / a; + d8[o + 1] = g / a; + d8[o + 2] = b / a; + d8[o + 3] = a; + } + } + + return this.__apply__(out); + } + + /** + * Flips / mirrors the image horizontally or vertically + * @param direction The direction to flip + */ + flip(direction: 'horizontal' | 'vertical'): this { + const u32 = this.__u32__; + const {width, height} = this; + + if (direction === 'horizontal') { + for (let y = 0; y < height; y++) u32.subarray(y * width, (y + 1) * width).reverse(); + } else if (direction === 'vertical') { + for (let y = 0; y < height >> 1; y++) { + const top = u32.slice(y * width, (y + 1) * width); + u32.copyWithin(y * width, (height - 1 - y) * width, (height - y) * width); + u32.set(top, (height - 1 - y) * width); + } + } else throw new RangeError('Invalid flip direction'); + + return this; + } + + /** + * @private + * @param image + */ + /** @internal the bitmap as a plain byte view */ + protected __bytes__(): Uint8Array { + return new Uint8Array(this.bitmap.buffer, this.bitmap.byteOffset, this.bitmap.byteLength); + } + + /** @internal */ + protected __apply__(image: Image): this { + this.__width__ = image.__width__; + this.__height__ = image.__height__; + this.__bitmap__ = image.__bitmap__; + this.__view__ = image.__view__; + this.__u32__ = image.__u32__; + + return this; + } + + /** + * Creates a multi-point gradient generator + * @param colors The gradient points to use (e.g. `{0: 0xff0000ff, 1: 0x00ff00ff}`) + * @return The gradient generator. The function argument is the position in the gradient (0..1). + */ + static gradient(colors: Record): (position: number) => Color { + const entries = Object.entries(colors).sort((a, b) => Number(a[0]) - Number(b[0])); + const positions = entries.map(e => parseFloat(e[0])); + const values = entries.map(e => e[1]); + + if (positions.length === 0) throw new RangeError('Invalid gradient point count'); + else if (positions.length === 1) { + return () => values[0]; + } else if (positions.length === 2) { + const gradient = this.__gradient__(values[0], values[1]); + return position => { + if (position <= positions[0]) return values[0]; + if (position >= positions[1]) return values[1]; + return gradient((position - positions[0]) / (positions[1] - positions[0])); + }; + } + + const minDef = Math.min(...positions); + const maxDef = Math.max(...positions); + const gradients: Array<{ min: number, max: number, gradient: (position: number) => Color }> = []; + + for (let i = 0; i < positions.length; i++) { + let minPos = positions[i - 1]; + if (minPos === undefined) continue; + + let maxPos = positions[i]; + + let minVal = values[i - 1]; + if (minVal === undefined) minVal = values[i]; + + const maxVal = values[i]; + const gradient = this.__gradient__(minVal, maxVal); + + gradients.push({min: minPos, max: maxPos, gradient}); + } + + return position => { + if (position <= minDef) return gradients[0].gradient(0); + if (position >= maxDef) return gradients[gradients.length - 1].gradient(1); + + for (const gradient of gradients) + if (position >= gradient.min && position <= gradient.max) + return gradient.gradient((position - gradient.min) / (gradient.max - gradient.min)); + throw new RangeError(`Invalid gradient position: ${position}`); + }; + } + + /** + * Rounds the images corners, with antialiased edges + * @param [radius=min(width,height)/4] The radius of the corners + */ + roundCorners(radius: number = Math.min(this.width, this.height) / 4): this { + radius = Math.max(0, Math.min(radius, this.width / 2, this.height / 2)); + const size = Math.ceil(radius); + + // coverage of the top left corner, mirrored onto the others + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const coverage = pixelCoverage(x, y, (sx, sy) => sx >= radius || sy >= radius || (sx - radius) ** 2 + (sy - radius) ** 2 <= radius * radius); + if (coverage >= 1) continue; + + // corners can share the middle row/column of odd sized images + const corners = new Set([[x, y], [this.width - 1 - x, y], [x, this.height - 1 - y], [this.width - 1 - x, this.height - 1 - y]].map(([px, py]) => py * this.width + px)); + for (const pixel of corners) this.bitmap[pixel * 4 + 3] *= coverage; + } + } + + return this; + } + + /** + * @private + */ + /** @internal */ + protected static __gradient__(startColor: Color, endColor: Color): (position: number) => Color { + const sr = startColor >>> 24; + const sg = startColor >> 16 & 0xff; + const sb = startColor >> 8 & 0xff; + const sa = startColor & 0xff; + const er = (endColor >>> 24) - sr; + const eg = (endColor >> 16 & 0xff) - sg; + const eb = (endColor >> 8 & 0xff) - sb; + const ea = (endColor & 0xff) - sa; + + return position => { + const r = sr + position * er; + const g = sg + position * eg; + const b = sb + position * eb; + const a = sa + position * ea; + return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)) >>> 0; + }; + } + + /** + * Applies a fisheye (barrel distortion) effect + * @param [strength=2] The distortion strength (exponent of the radial mapping), higher values bulge the center more + */ + fisheye(strength = 2): this { + const r = new Image(this.width, this.height); + + const w = this.width; + const h = this.height; + const tu32 = this.__u32__; + const ru32 = r.__u32__; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const xco = (x + 0.5) / w - 0.5; + const yco = (y + 0.5) / h - 0.5; + const dfc = Math.sqrt(xco ** 2 + yco ** 2); + + if (dfc === 0) { + ru32[y * w + x] = tu32[y * w + x]; + continue; + } + + const dis = 2 * dfc ** strength; + const nx = Math.floor((dis * xco / dfc + 0.5) * w); + const ny = Math.floor((dis * yco / dfc + 0.5) * h); + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + + ru32[y * w + x] = tu32[ny * w + nx]; + } + } + + return this.__apply__(r); + } + + + /** + * Encodes the image into a PNG, with the smallest lossless color type (a palette for up to 256 colors, + * greyscale or RGB, alpha only if needed). Uses the runtime's native zlib when available + * @param compression The compression level (0-9). Default: `1` + * @param metadata Text metadata to include + * @return The encoded data + */ + encode(compression?: number, metadata?: PNGMetadata): Promise; + /** + * Encodes the image into a PNG with compression level 1 + * @param metadata Text metadata to include + * @return The encoded data + */ + encode(metadata?: PNGMetadata): Promise; + async encode(compression: number | PNGMetadata = 1, metadata: PNGMetadata = {}): Promise { + if (typeof compression === 'object' && compression !== null) { + metadata = compression; + compression = 1; + } + + const level = Math.min(9, Math.max(0, ~~compression)); + const { + title, + author, + description, + copyright, + creationTime, + software, + disclaimer, + warning, + source, + comment + } = metadata || {}; + + return png.encodeRGBA(this.__bytes__(), this.width, this.height, level, { + Title: title, + Author: author, + Description: description, + Copyright: copyright, + 'Creation Time': new Date(creationTime === undefined ? Date.now() : creationTime).toUTCString(), + Software: software === undefined ? `github.com/matmen/ImageScript v${VERSION}` : software, + Disclaimer: disclaimer, + Warning: warning, + Source: source, + Comment: comment + }); + } + + /** + * Encodes the image into a PNG, an alias of {@link Image.encode} + */ + encodePNG(compression?: number, metadata?: PNGMetadata): Promise; + encodePNG(metadata?: PNGMetadata): Promise; + encodePNG(compression?: number | PNGMetadata, metadata?: PNGMetadata): Promise { + return this.encode(compression as number, metadata); + } + + /** + * Encodes the image into a JPEG + * @param [quality=90] The JPEG quality to use (1-100), chroma subsampling is disabled above 90 + */ + async encodeJPEG(quality = 90): Promise { + return jpeg.encode(this.__bytes__(), this.width, this.height, quality); + } + + /** + * Decodes an image (PNG, JPEG or TIFF) + * @param input The binary data to decode + * @param options See {@link DecodeOptions}. JPEGs, TIFFs and PNGs are rotated/mirrored as their orientation tag + * (Exif, for PNGs also XMP) says by default: photos taken with a rotated camera are stored sideways + * @return The decoded image + */ + static async decode(input: BinaryData, options: DecodeOptions = {}): Promise { + const { applyOrientation = true } = options; + const data = view(input); + const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength); + + let decoded: { width: number, height: number, pixels: Uint8Array, orientation?: number }; + if (ImageType.isPNG(dataView)) decoded = await png.decode(data); + else if (ImageType.isJPEG(dataView)) decoded = jpeg.decode(data); + else if (ImageType.isTIFF(dataView)) decoded = await tiff.decode(data); + else throw new Error('Unsupported image type'); + + const image = new Image(decoded.width, decoded.height); + image.bitmap.set(decoded.pixels); + return applyOrientation && decoded.orientation ? image.__orient__(decoded.orientation) : image; + } + + /** + * Applies an Exif orientation (1-8), turning the stored pixels into the displayed ones + * @internal + */ + protected __orient__(orientation: number): this { + if (orientation <= 1 || orientation > 8) return this; + + const w = this.width, h = this.height; + const swap = orientation >= 5; + const out = new Image(swap ? h : w, swap ? w : h); + const src = this.__u32__, dst = out.__u32__; + const ow = out.width, oh = out.height; + + // source pixel of every output pixel (X, Y) + for (let Y = 0; Y < oh; Y++) { + for (let X = 0; X < ow; X++) { + let x: number, y: number; + switch (orientation) { + case 2: x = w - 1 - X; y = Y; break; // mirrored horizontally + case 3: x = w - 1 - X; y = h - 1 - Y; break; // rotated 180 + case 4: x = X; y = h - 1 - Y; break; // mirrored vertically + case 5: x = Y; y = X; break; // transposed + case 6: x = Y; y = h - 1 - X; break; // needs a 90 degree clockwise rotation + case 7: x = w - 1 - Y; y = h - 1 - X; break; // transversed + default: x = w - 1 - Y; y = X; break; // 8, needs a 90 degree counterclockwise rotation + } + dst[Y * ow + X] = src[y * w + x]; + } + } + + return this.__apply__(out); + } + + /** + * Scale the SVG by the given amount. For use with {@link Image.renderSVG} + */ + static get SVG_MODE_SCALE(): 1 { + return 1; + } + + /** + * Scale the SVG to fit the given width. For use with {@link Image.renderSVG} + */ + static get SVG_MODE_WIDTH(): 2 { + return 2; + } + + /** + * Scale the SVG to fit the given height. For use with {@link Image.renderSVG} + */ + static get SVG_MODE_HEIGHT(): 3 { + return 3; + } + + /** + * Creates a new image from the given SVG + * Supports shapes, paths, transforms, use/symbol, CSS styles, gradients, strokes, clip paths, masks and opacity; + * text, images, filters and patterns are not rendered + * @param svg The SVG content + * @param size The size to use + * @param mode The SVG resizing mode to use (one of {@link SVG_MODE_SCALE}, {@link SVG_MODE_WIDTH}, {@link SVG_MODE_HEIGHT}) + * @return The rendered SVG graphic + */ + static async renderSVG(svg: string | BinaryData, size = 1, mode: SVGScaleMode = Image.SVG_MODE_SCALE): Promise { + if (![1, 2, 3].includes(mode)) + throw new Error('Invalid SVG scaling mode'); + + if (mode === Image.SVG_MODE_SCALE && size <= 0) + throw new RangeError('SVG scale must be > 0'); + if (mode !== Image.SVG_MODE_SCALE && size < 1) + throw new RangeError('SVG size must be >= 1'); + + const {width, height, pixels} = svgRenderer.render(typeof svg === 'string' ? svg : view(svg), mode, size); + const image = new Image(width, height); + image.bitmap.set(pixels); + + return image; + } + + /** + * Creates a new image containing the rendered text. + * @param font TrueType (ttf/ttc) or OpenType (otf) font buffer to use + * @param size Font size in pixels (per em) + * @param text Text to render + * @param [color=0xffffffff] Text color to use + * @param [layout] The text layout to use + * @param style Outline, see {@link TextStyle}; the image grows by the outline width on every side + * @return The rendered text + */ + static async renderText(font: BinaryData, size: number, text: string, color: Color = 0xffffffff, layout: TextLayout = new TextLayout(), style: TextStyle = {}): Promise { + const { strokeWidth = 0, strokeColor = 0x000000ff } = style; + if (isNaN(strokeWidth) || strokeWidth < 0) + throw new RangeError('Invalid stroke width'); + + const rendered = textRenderer.render(view(font), size, text, layout); + const [r, g, b, a] = Image.colorToRGBA(color); + + // the outline needs room around the glyphs + const pad = Math.ceil(strokeWidth); + const width = rendered.width + 2 * pad, height = rendered.height + 2 * pad; + const coverage = new Uint8Array(width * height); + for (let y = 0; y < rendered.height; y++) + coverage.set(rendered.coverage.subarray(y * rendered.width, (y + 1) * rendered.width), (y + pad) * width + pad); + + const image = new Image(width, height); + const bitmap = image.bitmap; + + if (!strokeWidth) { + for (let i = 0; i < coverage.length; i++) { + if (!coverage[i]) continue; + bitmap[i * 4] = r; + bitmap[i * 4 + 1] = g; + bitmap[i * 4 + 2] = b; + bitmap[i * 4 + 3] = coverage[i] * a / 255; + } + return image; + } + + const stroke = outline(coverage, width, height, strokeWidth); + const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor); + + // the glyphs drawn over their outline + for (let i = 0; i < coverage.length; i++) { + const af = coverage[i] / 255 * a / 255, as = stroke[i] * sa / 255; + const ao = af + as * (1 - af); + if (ao <= 0) continue; + + bitmap[i * 4] = (r * af + sr * as * (1 - af)) / ao; + bitmap[i * 4 + 1] = (g * af + sg * as * (1 - af)) / ao; + bitmap[i * 4 + 2] = (b * af + sb * as * (1 - af)) / ao; + bitmap[i * 4 + 3] = ao * 255; + } + + return image; + } + +} + +// samples per pixel axis for antialiased edges +const SUBSAMPLES = 4; + +// share of the pixel's area for which inside(x, y) holds, estimated on a regular grid +function pixelCoverage(px: number, py: number, inside: (x: number, y: number) => boolean): number { + let count = 0; + for (let j = 0; j < SUBSAMPLES; j++) + for (let i = 0; i < SUBSAMPLES; i++) + if (inside(px + (i + 0.5) / SUBSAMPLES, py + (j + 0.5) / SUBSAMPLES)) count++; + return count / (SUBSAMPLES * SUBSAMPLES); +} + +// share of the pixel covered by a circle, only edge pixels are sampled +function circleCoverage(px: number, py: number, cx: number, cy: number, radius: number): number { + const d = Math.hypot(px + 0.5 - cx, py + 0.5 - cy); + if (d <= radius - Math.SQRT1_2) return 1; + if (d >= radius + Math.SQRT1_2) return 0; + return pixelCoverage(px, py, (x, y) => (x - cx) ** 2 + (y - cy) ** 2 <= radius * radius); +} + +// rounds and clamps a channel value to 0..255 (NaN becomes 0) +function channel(value: number): number { + return value > 255 ? 255 : value > 0 ? Math.round(value) : 0; +} + +interface HSLA { + h: number; + s: number; + l: number; + a: number; +} + +// wraps a hue into 0..1 (negative hues included), exactly like (h % 1 + 1) % 1 but without the slow float modulo +function wrapHue(h: number): number { + const t = h - Math.trunc(h) + 1; + return t < 1 ? t : t < 2 ? t - 1 : 0; +} + +function hue2rgb(p: number, q: number, t: number): number { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; +} + +// separable blend functions of normalized channel values (W3C Compositing and Blending Level 1) +const hardLight = (b: number, s: number): number => s <= 0.5 ? b * 2 * s : 1 - (1 - b) * (1 - (2 * s - 1)); + +const BLEND_MODES: Record number> = { + 'normal': (_b, s) => s, + 'multiply': (b, s) => b * s, + 'screen': (b, s) => b + s - b * s, + 'overlay': (b, s) => hardLight(s, b), + 'darken': (b, s) => Math.min(b, s), + 'lighten': (b, s) => Math.max(b, s), + 'color-dodge': (b, s) => b === 0 ? 0 : s === 1 ? 1 : Math.min(1, b / (1 - s)), + 'color-burn': (b, s) => b === 1 ? 1 : s === 0 ? 0 : 1 - Math.min(1, (1 - b) / s), + 'hard-light': hardLight, + 'soft-light': (b, s) => { + if (s <= 0.5) return b - (1 - 2 * s) * b * (1 - b); + const d = b <= 0.25 ? ((16 * b - 12) * b + 4) * b : Math.sqrt(b); + return b + (2 * s - 1) * (d - b); + }, + 'difference': (b, s) => Math.abs(b - s), + 'exclusion': (b, s) => b + s - 2 * b * s, + 'add': (b, s) => Math.min(1, b + s) +}; + +/** + * Grows a coverage mask by a radius, using the distance of every pixel to the glyphs (an exact euclidean distance + * transform, so the cost doesn't depend on the radius) + * @return the outline's coverage (0..1) per pixel, including the area under the glyphs + */ +function outline(coverage: Uint8Array, width: number, height: number, radius: number): Float32Array { + const count = width * height; + // squared distance to the nearest pixel that is at least half covered + const distance = new Float64Array(count); + for (let i = 0; i < count; i++) distance[i] = coverage[i] >= 128 ? 0 : Infinity; + + const size = Math.max(width, height); + const line = new Float64Array(size), result = new Float64Array(size); + const parabolas = new Int32Array(size), bounds = new Float64Array(size + 1); + + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) line[y] = distance[y * width + x]; + distanceTransform(line, height, result, parabolas, bounds); + for (let y = 0; y < height; y++) distance[y * width + x] = result[y]; + } + + for (let y = 0; y < height; y++) { + const row = y * width; + for (let x = 0; x < width; x++) line[x] = distance[row + x]; + distanceTransform(line, width, result, parabolas, bounds); + for (let x = 0; x < width; x++) distance[row + x] = result[x]; + } + + const out = new Float32Array(count); + for (let i = 0; i < count; i++) { + // pixels next to the glyph edge are covered by the glyph's own antialiasing + const d = Math.sqrt(distance[i]); + out[i] = Math.max(coverage[i] / 255, Math.min(1, Math.max(0, radius + 0.5 - d))); + } + + return out; +} + +// 1D squared euclidean distance transform (Felzenszwalb & Huttenlocher): lower envelope of parabolas +function distanceTransform(f: Float64Array, n: number, d: Float64Array, v: Int32Array, z: Float64Array): void { + let k = -1; + for (let q = 0; q < n; q++) { + if (f[q] === Infinity) continue; + let s = 0; + while (k >= 0) { + s = (f[q] + q * q - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); + if (s > z[k]) break; + k--; + } + k++; + v[k] = q; + z[k] = k === 0 ? -Infinity : s; + z[k + 1] = Infinity; + } + + if (k < 0) { + d.fill(Infinity, 0, n); + return; + } + + for (let q = 0, j = 0; q < n; q++) { + while (z[j + 1] < q) j++; + d[q] = (q - v[j]) ** 2 + f[v[j]]; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..573462e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,25 @@ +import { BinaryData } from './util/mem.ts'; +import { Image } from './image.ts'; +import { GIF } from './gif.ts'; +import { ImageType } from './image-type.ts'; + +export { Image } from './image.ts'; +export type { Color, ColorFunction, ResizeMode, SVGScaleMode, PNGMetadata, TextStyle, DecodeOptions, CompositeOptions, BlendMode, CropCircleOptions, DominantColorOptions, RotateOptions } from './image.ts'; +export { Frame } from './frame.ts'; +export type { FrameDisposalModeName, FrameDisposalModeId } from './frame.ts'; +export { GIF } from './gif.ts'; +export { TextLayout } from './text-layout.ts'; +export type { TextLayoutOptions, WrapStyle, HorizontalAlign, VerticalAlign } from './text-layout.ts'; +export { ImageType } from './image-type.ts'; +export type { ImageTypeName } from './image-type.ts'; +export type { BinaryData } from './util/mem.ts'; + +/** + * Decodes any supported image, GIFs are returned as {@link GIF} + * @param data The image data + * @param onlyExtractFirstFrame Whether to end GIF decoding after the first frame + */ +export function decode(data: BinaryData, onlyExtractFirstFrame = false): Promise { + if (ImageType.getType(data) === 'gif') return GIF.decode(data, onlyExtractFirstFrame); + return Image.decode(data); +} diff --git a/src/render/font.ts b/src/render/font.ts new file mode 100644 index 0000000..b95a3e4 --- /dev/null +++ b/src/render/font.ts @@ -0,0 +1,919 @@ + +/** + * A path command in font units (y up) or user units: + * ['M', x, y], ['L', x, y], ['Q', cx, cy, x, y], ['C', c1x, c1y, c2x, c2y, x, y], ['Z'] + */ +export type PathCommand = [string, ...number[]]; + +interface CFF { + charStrings: Uint8Array[]; + globalSubrs: Uint8Array[]; + privateFor: (glyph: number) => { subrs: Uint8Array[] }; +} + +type Dict = Record; + +/** + * Parses TrueType (glyf) and OpenType (CFF) fonts, including the first font of a collection + */ +export class Font { + data: Uint8Array; + view: DataView; + tables: Record; + unitsPerEm: number; + indexToLocFormat: number; + ascender: number; + descender: number; + lineGap: number; + numberOfHMetrics: number; + numGlyphs: number; + outlines: 'glyf' | 'cff'; + cff: CFF | null = null; + cmap: (codepoint: number) => number; + outlineCache: Map; + /** kerning between two glyphs in font units, null if the font has none */ + kerning: ((left: number, right: number) => number) | null; + + constructor(data: Uint8Array) { + this.data = data; + this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); + + let offset = 0; + const signature = this.u32(0); + if (signature === 0x74746366) offset = this.u32(12); // 'ttcf', use the first font + else if (signature === 0x774f4646 || signature === 0x774f4632) throw new Error('Font: WOFF fonts are not supported'); + + const version = this.u32(offset); + if (version !== 0x00010000 && version !== 0x4f54544f && version !== 0x74727565) throw new Error('Font: invalid font file'); + + this.tables = {}; + const count = this.u16(offset + 4); + for (let i = 0; i < count; i++) { + const record = offset + 12 + i * 16; + const tag = String.fromCharCode(...data.subarray(record, record + 4)); + this.tables[tag] = { offset: this.u32(record + 8), length: this.u32(record + 12) }; + } + + for (const tag of ['head', 'hhea', 'hmtx', 'maxp', 'cmap']) + if (!this.tables[tag]) throw new Error(`Font: missing ${tag} table`); + + const head = this.tables.head.offset; + this.unitsPerEm = this.u16(head + 18); + this.indexToLocFormat = this.i16(head + 50); + + const hhea = this.tables.hhea.offset; + this.ascender = this.i16(hhea + 4); + this.descender = this.i16(hhea + 6); + this.lineGap = this.i16(hhea + 8); + this.numberOfHMetrics = this.u16(hhea + 34); + + this.numGlyphs = this.u16(this.tables.maxp.offset + 4); + + if (this.tables.glyf && this.tables.loca) this.outlines = 'glyf'; + else if (this.tables['CFF ']) { + this.outlines = 'cff'; + this.cff = parseCFF(this, this.tables['CFF '].offset); + } else if (this.tables.CFF2) throw new Error('Font: CFF2 outlines are not supported'); + else throw new Error('Font: no supported glyph outlines'); + + this.cmap = parseCmap(this, this.tables.cmap.offset); + this.outlineCache = new Map(); + this.kerning = (this.tables.GPOS && parseGPOSKerning(this, this.tables.GPOS.offset)) || (this.tables.kern && parseKern(this, this.tables.kern.offset)) || null; + } + + u8(o: number): number { return this.data[o]; } + i8(o: number): number { return this.view.getInt8(o); } + u16(o: number): number { return this.view.getUint16(o); } + i16(o: number): number { return this.view.getInt16(o); } + u32(o: number): number { return this.view.getUint32(o); } + i32(o: number): number { return this.view.getInt32(o); } + + /** + * @param {number} codepoint + * @return {number} glyph index, 0 (.notdef) if the font has no glyph for it + */ + glyphIndex(codepoint: number): number { + return this.cmap(codepoint) || 0; + } + + /** + * @param {number} glyph + * @return {number} advance width in font units + */ + advance(glyph: number): number { + const hmtx = this.tables.hmtx.offset; + const index = Math.min(glyph, this.numberOfHMetrics - 1); + return this.u16(hmtx + index * 4); + } + + /** + * @param {number} glyph + * @return {Array} path commands in font units + */ + outline(glyph: number): PathCommand[] { + if (glyph >= this.numGlyphs) glyph = 0; + let path = this.outlineCache.get(glyph); + if (!path) { + path = this.outlines === 'glyf' ? this.glyfOutline(glyph, 0) : this.cffOutline(glyph); + this.outlineCache.set(glyph, path); + } + return path; + } + + glyfOutline(glyph: number, depth: number): PathCommand[] { + if (depth > 8) return []; + + const loca = this.tables.loca.offset; + const start = this.indexToLocFormat ? this.u32(loca + glyph * 4) : this.u16(loca + glyph * 2) * 2; + const end = this.indexToLocFormat ? this.u32(loca + glyph * 4 + 4) : this.u16(loca + glyph * 2 + 2) * 2; + if (end <= start) return []; + + const offset = this.tables.glyf.offset + start; + const contours = this.i16(offset); + + return contours >= 0 ? this.simpleGlyph(offset, contours) : this.compositeGlyph(offset, depth); + } + + simpleGlyph(offset: number, contourCount: number): PathCommand[] { + const endPoints = []; + for (let i = 0; i < contourCount; i++) endPoints.push(this.u16(offset + 10 + i * 2)); + const pointCount = contourCount ? endPoints[contourCount - 1] + 1 : 0; + + let p = offset + 10 + contourCount * 2; + p += 2 + this.u16(p); // skip instructions + + const flags = new Uint8Array(pointCount); + for (let i = 0; i < pointCount;) { + const flag = this.u8(p++); + flags[i++] = flag; + if (flag & 8) { + for (let repeat = this.u8(p++); repeat > 0 && i < pointCount; repeat--) flags[i++] = flag; + } + } + + const xs = new Int32Array(pointCount); + const ys = new Int32Array(pointCount); + for (const [coords, short, same] of [[xs, 2, 16], [ys, 4, 32]] as Array<[Int32Array, number, number]>) { + let value = 0; + for (let i = 0; i < pointCount; i++) { + const flag = flags[i]; + if (flag & short) { + const delta = this.u8(p++); + value += flag & same ? delta : -delta; + } else if (!(flag & same)) { + value += this.i16(p); + p += 2; + } + coords[i] = value; + } + } + + const path: PathCommand[] = []; + let first = 0; + for (const last of endPoints) { + contourToPath(path, xs, ys, flags, first, last); + first = last + 1; + } + + return path; + } + + compositeGlyph(offset: number, depth: number): PathCommand[] { + const path: PathCommand[] = []; + let p = offset + 10; + + for (let more = true; more;) { + const flags = this.u16(p); + const glyph = this.u16(p + 2); + p += 4; + + let dx, dy; + if (flags & 1) { + dx = flags & 2 ? this.i16(p) : this.u16(p); + dy = flags & 2 ? this.i16(p + 2) : this.u16(p + 2); + p += 4; + } else { + dx = flags & 2 ? this.i8(p) : this.u8(p); + dy = flags & 2 ? this.i8(p + 1) : this.u8(p + 1); + p += 2; + } + + // point matching (ARGS_ARE_XY_VALUES unset) is rare, treat it as no offset + if (!(flags & 2)) dx = dy = 0; + + let a = 1, b = 0, c = 0, d = 1; + const f2dot14 = (o: number): number => this.i16(o) / 16384; + if (flags & 8) { + a = d = f2dot14(p); + p += 2; + } else if (flags & 0x40) { + a = f2dot14(p); + d = f2dot14(p + 2); + p += 4; + } else if (flags & 0x80) { + a = f2dot14(p); + b = f2dot14(p + 2); + c = f2dot14(p + 4); + d = f2dot14(p + 6); + p += 8; + } + + for (const command of this.glyfOutline(glyph, depth + 1)) { + const transformed: PathCommand = [command[0]]; + for (let i = 1; i < command.length; i += 2) { + const x = command[i] as number, y = command[i + 1] as number; + transformed.push(a * x + c * y + dx, b * x + d * y + dy); + } + path.push(transformed); + } + + more = !!(flags & 0x20); + } + + return path; + } + + cffOutline(glyph: number): PathCommand[] { + return cffCharString(this.cff as CFF, glyph); + } +} + +// converts a TrueType contour (quadratic, with implied on-curve points) to path commands +function contourToPath(path: PathCommand[], xs: Int32Array, ys: Int32Array, flags: Uint8Array, first: number, last: number): void { + if (last < first) return; + const count = last - first + 1; + const on = (i: number): number => flags[first + (i % count)] & 1; + const x = (i: number): number => xs[first + (i % count)]; + const y = (i: number): number => ys[first + (i % count)]; + + // find a starting point that is on the curve, or synthesize one between two off-curve points + let start = -1; + for (let i = 0; i < count; i++) + if (on(i)) { + start = i; + break; + } + + let sx, sy; + if (start === -1) { + sx = (x(0) + x(1)) / 2; + sy = (y(0) + y(1)) / 2; + start = 1; + } else { + sx = x(start); + sy = y(start); + start++; + } + + path.push(['M', sx, sy]); + + // pending off-curve control point + let cx = 0, cy = 0, pending = false; + for (let n = 0; n < count; n++) { + const i = start + n; + const px = x(i), py = y(i); + + if (on(i)) { + if (!pending) path.push(['L', px, py]); + else { + path.push(['Q', cx, cy, px, py]); + pending = false; + } + } else { + if (pending) { + const mx = (cx + px) / 2, my = (cy + py) / 2; + path.push(['Q', cx, cy, mx, my]); + } + cx = px; + cy = py; + pending = true; + } + } + + if (pending) path.push(['Q', cx, cy, sx, sy]); + path.push(['Z']); +} + +function parseCmap(font: Font, offset: number): (codepoint: number) => number { + const count = font.u16(offset + 2); + const subtables: Array<{ platform: number, encoding: number, table: number, format: number }> = []; + + for (let i = 0; i < count; i++) { + const record = offset + 4 + i * 8; + const platform = font.u16(record); + const encoding = font.u16(record + 2); + const table = offset + font.u32(record + 4); + subtables.push({ platform, encoding, table, format: font.u16(table) }); + } + + const rank = ({ platform, encoding, format }: { platform: number, encoding: number, format: number }): number => { + if (![0, 4, 6, 12].includes(format)) return -1; + if (platform === 3 && encoding === 10) return 6; + if (platform === 0 && (encoding === 4 || encoding === 6)) return 5; + if (platform === 3 && encoding === 1) return 4; + if (platform === 0) return 3; + if (platform === 3 && encoding === 0) return 2; + if (platform === 1 && encoding === 0) return 1; + return -1; + }; + + const best = subtables.filter(s => rank(s) >= 0).sort((a, b) => rank(b) - rank(a))[0]; + if (!best) throw new Error('Font: no supported character map'); + + const { table, format, platform, encoding } = best; + const symbol = platform === 3 && encoding === 0; + + let lookup: (c: number) => number; + if (format === 0) lookup = (c: number): number => c < 256 ? font.u8(table + 6 + c) : 0; + else if (format === 6) { + const first = font.u16(table + 6), entries = font.u16(table + 8); + lookup = (c: number): number => c >= first && c < first + entries ? font.u16(table + 10 + (c - first) * 2) : 0; + } else if (format === 4) { + const segments = font.u16(table + 6) / 2; + const ends = table + 14; + const starts = ends + segments * 2 + 2; + const deltas = starts + segments * 2; + const rangeOffsets = deltas + segments * 2; + + lookup = (c: number): number => { + if (c > 0xffff) return 0; + // binary search over segment end codes + let lo = 0, hi = segments - 1; + while (lo < hi) { + const mid = lo + hi >> 1; + if (font.u16(ends + mid * 2) < c) lo = mid + 1; + else hi = mid; + } + + const start = font.u16(starts + lo * 2); + if (c < start || c > font.u16(ends + lo * 2)) return 0; + + const delta = font.u16(deltas + lo * 2); + const rangeOffset = font.u16(rangeOffsets + lo * 2); + if (!rangeOffset) return c + delta & 0xffff; + + const glyph = font.u16(rangeOffsets + lo * 2 + rangeOffset + (c - start) * 2); + return glyph ? glyph + delta & 0xffff : 0; + }; + } else { + const groups = font.u32(table + 12); + lookup = (c: number): number => { + let lo = 0, hi = groups - 1; + while (lo <= hi) { + const mid = lo + hi >> 1; + const group = table + 16 + mid * 12; + if (c < font.u32(group)) hi = mid - 1; + else if (c > font.u32(group + 4)) lo = mid + 1; + else return font.u32(group + 8) + c - font.u32(group); + } + return 0; + }; + } + + // symbol fonts map their characters to U+F000-U+F0FF + return symbol ? (c: number): number => lookup(c) || (c < 256 ? lookup(0xf000 + c) : 0) : lookup; +} + +// --------------------------------------------------------------------------- +// kerning +// --------------------------------------------------------------------------- + +type PairLookup = (left: number, right: number) => number | undefined; + +// horizontal pair adjustments of the GPOS 'kern' feature (pair positioning lookups, also behind extension lookups) +function parseGPOSKerning(font: Font, gpos: number): ((left: number, right: number) => number) | null { + const featureList = gpos + font.u16(gpos + 6); + const lookupList = gpos + font.u16(gpos + 8); + + const lookups = new Set(); + for (let i = 0, count = font.u16(featureList); i < count; i++) { + const record = featureList + 2 + i * 6; + if (font.u32(record) !== 0x6b65726e) continue; // 'kern' + const feature = featureList + font.u16(record + 4); + for (let j = 0, n = font.u16(feature + 2); j < n; j++) lookups.add(font.u16(feature + 4 + j * 2)); + } + + // per lookup the subtables are tried in order, the first one covering the pair applies + const pairLookups: PairLookup[][] = []; + for (const index of [...lookups].sort((a, b) => a - b)) { + if (index >= font.u16(lookupList)) continue; + const lookup = lookupList + font.u16(lookupList + 2 + index * 2); + const type = font.u16(lookup); + const subtables: PairLookup[] = []; + + for (let i = 0, count = font.u16(lookup + 4); i < count; i++) { + let subtable = lookup + font.u16(lookup + 6 + i * 2); + if (type === 9) { + if (font.u16(subtable + 2) !== 2) continue; + subtable += font.u32(subtable + 4); + } else if (type !== 2) continue; + + const pair = parsePairPos(font, subtable); + if (pair) subtables.push(pair); + } + + if (subtables.length) pairLookups.push(subtables); + } + + if (!pairLookups.length) return null; + return (left, right) => { + let total = 0; + for (const subtables of pairLookups) { + for (const subtable of subtables) { + const value = subtable(left, right); + if (value === undefined) continue; + total += value; + break; + } + } + return total; + }; +} + +// size of a value record and the offset of its XAdvance field (-1 if absent) +function valueRecord(format: number): { size: number, xAdvance: number } { + let size = 0, xAdvance = -1; + for (let bit = 0; bit < 8; bit++) { + if (!(format & 1 << bit)) continue; + if (bit === 2) xAdvance = size; + size += 2; + } + return { size, xAdvance }; +} + +function parsePairPos(font: Font, table: number): PairLookup | null { + const format = font.u16(table); + const coverage = parseCoverage(font, table + font.u16(table + 2)); + const first = valueRecord(font.u16(table + 4)); + const second = valueRecord(font.u16(table + 6)); + if (first.xAdvance === -1) return null; + + if (format === 1) { + const sets = font.u16(table + 8); + const recordSize = 2 + first.size + second.size; + + return (left, right) => { + const index = coverage(left); + if (index === -1 || index >= sets) return undefined; + const set = table + font.u16(table + 10 + index * 2); + + // pair value records are sorted by the second glyph + let lo = 0, hi = font.u16(set) - 1; + while (lo <= hi) { + const mid = lo + hi >> 1; + const record = set + 2 + mid * recordSize; + const glyph = font.u16(record); + if (glyph < right) lo = mid + 1; + else if (glyph > right) hi = mid - 1; + else return font.i16(record + 2 + first.xAdvance); + } + return undefined; + }; + } + + if (format === 2) { + const classes1 = parseClassDef(font, table + font.u16(table + 8)); + const classes2 = parseClassDef(font, table + font.u16(table + 10)); + const class1Count = font.u16(table + 12), class2Count = font.u16(table + 14); + const recordSize = first.size + second.size; + + return (left, right) => { + if (coverage(left) === -1) return undefined; + const c1 = classes1(left), c2 = classes2(right); + if (c1 >= class1Count || c2 >= class2Count) return undefined; + return font.i16(table + 16 + (c1 * class2Count + c2) * recordSize + first.xAdvance); + }; + } + + return null; +} + +// returns the coverage index of a glyph, -1 if it isn't covered +function parseCoverage(font: Font, table: number): (glyph: number) => number { + const format = font.u16(table), count = font.u16(table + 2); + + return glyph => { + let lo = 0, hi = count - 1; + while (lo <= hi) { + const mid = lo + hi >> 1; + if (format === 1) { + const value = font.u16(table + 4 + mid * 2); + if (value < glyph) lo = mid + 1; + else if (value > glyph) hi = mid - 1; + else return mid; + } else { + const range = table + 4 + mid * 6; + if (font.u16(range + 2) < glyph) lo = mid + 1; + else if (font.u16(range) > glyph) hi = mid - 1; + else return font.u16(range + 4) + glyph - font.u16(range); + } + } + return -1; + }; +} + +function parseClassDef(font: Font, table: number): (glyph: number) => number { + const format = font.u16(table); + + if (format === 1) { + const start = font.u16(table + 2), count = font.u16(table + 4); + return glyph => glyph >= start && glyph < start + count ? font.u16(table + 6 + (glyph - start) * 2) : 0; + } + + const count = font.u16(table + 2); + return glyph => { + let lo = 0, hi = count - 1; + while (lo <= hi) { + const mid = lo + hi >> 1; + const range = table + 4 + mid * 6; + if (font.u16(range + 2) < glyph) lo = mid + 1; + else if (font.u16(range) > glyph) hi = mid - 1; + else return font.u16(range + 4); + } + return 0; + }; +} + +// the (Microsoft) kern table, format 0 subtables with horizontal kerning +function parseKern(font: Font, kern: number): ((left: number, right: number) => number) | null { + if (font.u16(kern) !== 0) return null; + + const tables: Array<{ pairs: number, start: number }> = []; + let offset = kern + 4; + for (let i = 0, count = font.u16(kern + 2); i < count; i++) { + const coverage = font.u16(offset + 4); + // big format 0 subtables overflow their 16-bit length field, so it is computed from the pair count + const length = coverage >> 8 === 0 ? 14 + font.u16(offset + 6) * 6 : font.u16(offset + 2); + // horizontal, no minimum values, not cross stream, format 0 + if ((coverage & 0xff0f) === 1) tables.push({ pairs: font.u16(offset + 6), start: offset + 14 }); + offset += length; + } + + if (!tables.length) return null; + return (left, right) => { + const key = (left << 16 | right) >>> 0; + let total = 0; + for (const { pairs, start } of tables) { + let lo = 0, hi = pairs - 1; + while (lo <= hi) { + const mid = lo + hi >> 1; + const pair = start + mid * 6; + const value = font.u32(pair); + if (value < key) lo = mid + 1; + else if (value > key) hi = mid - 1; + else { + total += font.i16(pair + 4); + break; + } + } + } + return total; + }; +} + +// --------------------------------------------------------------------------- +// CFF +// --------------------------------------------------------------------------- + +function parseCFF(font: Font, base: number): CFF { + const data = font.data; + + const readIndex = (offset: number): { items: Uint8Array[], end: number } => { + const count = font.u16(offset); + if (count === 0) return { items: [], end: offset + 2 }; + + const offSize = data[offset + 2]; + const readOffset = (o: number): number => { + let v = 0; + for (let i = 0; i < offSize; i++) v = v * 256 + data[o + i]; + return v; + }; + + const dataStart = offset + 3 + (count + 1) * offSize - 1; + const items: Uint8Array[] = []; + for (let i = 0; i < count; i++) { + const start = dataStart + readOffset(offset + 3 + i * offSize); + const end = dataStart + readOffset(offset + 3 + (i + 1) * offSize); + items.push(data.subarray(start, end)); + } + + return { items, end: dataStart + readOffset(offset + 3 + count * offSize) }; + }; + + const headerSize = data[base + 2]; + const names = readIndex(base + headerSize); + const topDicts = readIndex(names.end); + const strings = readIndex(topDicts.end); + const globalSubrs = readIndex(strings.end).items; + + const top = parseDict(topDicts.items[0]); + if (!top[17]) throw new Error('Font: CFF font has no charstrings'); + + const charStrings = readIndex(base + top[17][0]).items; + + const readPrivate = (entry: number[] | undefined): { subrs: Uint8Array[] } => { + if (!entry) return { subrs: [] }; + const [size, offset] = entry; + const dict = parseDict(data.subarray(base + offset, base + offset + size)); + const subrs = dict[19] ? readIndex(base + offset + dict[19][0]).items : []; + return { subrs }; + }; + + // CID-keyed fonts select a private dict per glyph + let privateFor: CFF['privateFor']; + if (top[1236]) { + const fdArray = readIndex(base + top[1236][0]).items.map(dict => readPrivate(parseDict(dict)[18])); + const fdSelect = base + top[1237][0]; + const format = data[fdSelect]; + + privateFor = glyph => { + if (format === 0) return fdArray[data[fdSelect + 1 + glyph]]; + const ranges = font.u16(fdSelect + 1); + for (let i = 0; i < ranges; i++) { + const r = fdSelect + 3 + i * 3; + const next = font.u16(r + 3); + if (glyph >= font.u16(r) && glyph < next) return fdArray[data[r + 2]]; + } + return fdArray[0]; + }; + } else { + const shared = readPrivate(top[18]); + privateFor = () => shared; + } + + return { charStrings, globalSubrs, privateFor }; +} + +// returns operator (escaped operators as 1200 + n) -> operands +function parseDict(bytes: Uint8Array): Dict { + const dict: Dict = {}; + const operands: number[] = []; + + for (let i = 0; i < bytes.length;) { + const b0 = bytes[i++]; + + if (b0 <= 21) { + const op = b0 === 12 ? 1200 + bytes[i++] : b0; + dict[op] = operands.splice(0); + } else if (b0 === 28) { + operands.push((bytes[i] << 24 | bytes[i + 1] << 16) >> 16); + i += 2; + } else if (b0 === 29) { + operands.push(bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]); + i += 4; + } else if (b0 === 30) { + let s = ''; + for (let done = false; !done;) { + const byte = bytes[i++]; + for (const nibble of [byte >> 4, byte & 15]) { + if (nibble === 15) { + done = true; + break; + } + s += ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', '', '-'][nibble]; + } + } + operands.push(parseFloat(s)); + } else if (b0 >= 32 && b0 <= 246) operands.push(b0 - 139); + else if (b0 >= 247 && b0 <= 250) operands.push((b0 - 247) * 256 + bytes[i++] + 108); + else if (b0 >= 251 && b0 <= 254) operands.push(-(b0 - 251) * 256 - bytes[i++] - 108); + } + + return dict; +} + +function subrBias(count: number): number { + return count < 1240 ? 107 : count < 33900 ? 1131 : 32768; +} + +// Type 2 charstring interpreter +function cffCharString(cff: CFF, glyph: number): PathCommand[] { + const code = cff.charStrings[glyph]; + if (!code) return []; + + const { subrs } = cff.privateFor(glyph); + const globalBias = subrBias(cff.globalSubrs.length); + const localBias = subrBias(subrs.length); + + const path: PathCommand[] = []; + const stack: number[] = []; + let x = 0, y = 0; + let stems = 0; + let open = false; + let widthParsed = false; + let depth = 0; + + const moveTo = (dx: number, dy: number): void => { + if (open) path.push(['Z']); + x += dx; + y += dy; + path.push(['M', x, y]); + open = true; + }; + + const lineTo = (dx: number, dy: number): void => { + x += dx; + y += dy; + path.push(['L', x, y]); + }; + + const curveTo = (dx1: number, dy1: number, dx2: number, dy2: number, dx3: number, dy3: number): void => { + const c1x = x + dx1, c1y = y + dy1; + const c2x = c1x + dx2, c2y = c1y + dy2; + x = c2x + dx3; + y = c2y + dy3; + path.push(['C', c1x, c1y, c2x, c2y, x, y]); + }; + + // the first stack clearing operator may carry the advance width as an extra argument + const stripWidth = (expectedEven: boolean): void => { + if (!widthParsed && (stack.length % 2 === 1) === expectedEven) stack.shift(); + widthParsed = true; + }; + + const run = (code: Uint8Array): boolean => { + if (++depth > 10) return true; + + for (let i = 0; i < code.length;) { + const b0 = code[i++]; + + if (b0 >= 32 || b0 === 28) { + if (b0 === 28) { + stack.push((code[i] << 24 | code[i + 1] << 16) >> 16); + i += 2; + } else if (b0 <= 246) stack.push(b0 - 139); + else if (b0 <= 250) stack.push((b0 - 247) * 256 + code[i++] + 108); + else if (b0 <= 254) stack.push(-(b0 - 251) * 256 - code[i++] - 108); + else { + stack.push((code[i] << 24 | code[i + 1] << 16 | code[i + 2] << 8 | code[i + 3]) / 65536); + i += 4; + } + continue; + } + + switch (b0) { + case 1: case 3: case 18: case 23: // hstem, vstem, hstemhm, vstemhm + stripWidth(true); + stems += stack.length >> 1; + stack.length = 0; + break; + + case 19: case 20: // hintmask, cntrmask + stripWidth(true); + stems += stack.length >> 1; + stack.length = 0; + i += stems + 7 >> 3; + break; + + case 21: // rmoveto + stripWidth(true); + moveTo(stack[0], stack[1]); + stack.length = 0; + break; + + case 22: // hmoveto + stripWidth(false); + moveTo(stack[0], 0); + stack.length = 0; + break; + + case 4: // vmoveto + stripWidth(false); + moveTo(0, stack[0]); + stack.length = 0; + break; + + case 5: // rlineto + for (let j = 0; j + 1 < stack.length; j += 2) lineTo(stack[j], stack[j + 1]); + stack.length = 0; + break; + + case 6: case 7: { // hlineto, vlineto + let horizontal = b0 === 6; + for (const v of stack) { + if (horizontal) lineTo(v, 0); + else lineTo(0, v); + horizontal = !horizontal; + } + stack.length = 0; + break; + } + + case 8: // rrcurveto + for (let j = 0; j + 5 < stack.length; j += 6) curveTo(stack[j], stack[j + 1], stack[j + 2], stack[j + 3], stack[j + 4], stack[j + 5]); + stack.length = 0; + break; + + case 24: { // rcurveline + let j = 0; + for (; j + 5 < stack.length - 2; j += 6) curveTo(stack[j], stack[j + 1], stack[j + 2], stack[j + 3], stack[j + 4], stack[j + 5]); + lineTo(stack[j], stack[j + 1]); + stack.length = 0; + break; + } + + case 25: { // rlinecurve + let j = 0; + for (; j + 1 < stack.length - 6; j += 2) lineTo(stack[j], stack[j + 1]); + curveTo(stack[j], stack[j + 1], stack[j + 2], stack[j + 3], stack[j + 4], stack[j + 5]); + stack.length = 0; + break; + } + + case 26: { // vvcurveto + let j = 0, dx1 = 0; + if (stack.length % 4) dx1 = stack[j++]; + for (; j + 3 < stack.length; j += 4) { + curveTo(dx1, stack[j], stack[j + 1], stack[j + 2], 0, stack[j + 3]); + dx1 = 0; + } + stack.length = 0; + break; + } + + case 27: { // hhcurveto + let j = 0, dy1 = 0; + if (stack.length % 4) dy1 = stack[j++]; + for (; j + 3 < stack.length; j += 4) { + curveTo(stack[j], dy1, stack[j + 1], stack[j + 2], stack[j + 3], 0); + dy1 = 0; + } + stack.length = 0; + break; + } + + case 30: case 31: { // vhcurveto, hvcurveto + let horizontal = b0 === 31; + for (let j = 0; j + 3 < stack.length; j += 4) { + const last = j + 5 === stack.length ? stack[j + 4] : 0; + if (horizontal) curveTo(stack[j], 0, stack[j + 1], stack[j + 2], last, stack[j + 3]); + else curveTo(0, stack[j], stack[j + 1], stack[j + 2], stack[j + 3], last); + horizontal = !horizontal; + } + stack.length = 0; + break; + } + + case 10: case 29: { // callsubr, callgsubr + const local = b0 === 10; + const index = (stack.pop() as number) + (local ? localBias : globalBias); + const subr = (local ? subrs : cff.globalSubrs)[index]; + if (subr && run(subr)) return true; + break; + } + + case 11: // return + depth--; + return false; + + case 14: // endchar + stripWidth(true); + if (open) path.push(['Z']); + open = false; + return true; + + case 12: { // escaped operators: flex variants, everything else is ignored + const op = code[i++]; + const s = stack; + + if (op === 35) { // flex + curveTo(s[0], s[1], s[2], s[3], s[4], s[5]); + curveTo(s[6], s[7], s[8], s[9], s[10], s[11]); + } else if (op === 34) { // hflex + const startY = y; + curveTo(s[0], 0, s[1], s[2], s[3], 0); + curveTo(s[4], 0, s[5], startY - y, s[6], 0); + } else if (op === 36) { // hflex1 + const startY = y; + curveTo(s[0], s[1], s[2], s[3], s[4], 0); + curveTo(s[5], 0, s[6], s[7], s[8], startY - (y + s[7])); + } else if (op === 37) { // flex1 + const startX = x, startY = y; + let dx = 0, dy = 0; + for (let j = 0; j < 10; j += 2) { + dx += s[j]; + dy += s[j + 1]; + } + curveTo(s[0], s[1], s[2], s[3], s[4], s[5]); + const lastX = Math.abs(dx) > Math.abs(dy); + curveTo(s[6], s[7], s[8], s[9], lastX ? s[10] : startX - (x + s[6] + s[8]), lastX ? startY - (y + s[7] + s[9]) : s[10]); + } + + stack.length = 0; + break; + } + + default: + stack.length = 0; + } + } + + depth--; + return false; + }; + + run(code); + if (open) path.push(['Z']); + return path; +} + diff --git a/src/render/svg.ts b/src/render/svg.ts new file mode 100644 index 0000000..0a535b1 --- /dev/null +++ b/src/render/svg.ts @@ -0,0 +1,1894 @@ +import { decode as decodeUTF8 } from '../util/utf8.ts'; +import { PathCommand } from './font.ts'; + +/** + * SVG renderer supporting a static subset of SVG 1.1/2: + * shapes, paths, transforms, nested svg/use/symbol, presentation attributes, inline styles and simple + * + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/targets/circle.png b/tests/targets/circle.png index 084f3ae..7426df2 100644 Binary files a/tests/targets/circle.png and b/tests/targets/circle.png differ diff --git a/tests/targets/circle2.png b/tests/targets/circle2.png index 5e66634..9c36d00 100644 Binary files a/tests/targets/circle2.png and b/tests/targets/circle2.png differ diff --git a/tests/targets/circle3.png b/tests/targets/circle3.png index 27bb41d..d3863d9 100644 Binary files a/tests/targets/circle3.png and b/tests/targets/circle3.png differ diff --git a/tests/targets/circle4.png b/tests/targets/circle4.png index 00b15ad..a9f135b 100644 Binary files a/tests/targets/circle4.png and b/tests/targets/circle4.png differ diff --git a/tests/targets/crop.png b/tests/targets/crop.png index 795b2e1..c757da5 100644 Binary files a/tests/targets/crop.png and b/tests/targets/crop.png differ diff --git a/tests/targets/effects.png b/tests/targets/effects.png new file mode 100644 index 0000000..5e6cd7e Binary files /dev/null and b/tests/targets/effects.png differ diff --git a/tests/targets/fill-func.png b/tests/targets/fill-func.png index ea308a7..2a9f2fc 100644 Binary files a/tests/targets/fill-func.png and b/tests/targets/fill-func.png differ diff --git a/tests/targets/fill-static.png b/tests/targets/fill-static.png index 33ab5f2..d3fe73c 100644 Binary files a/tests/targets/fill-static.png and b/tests/targets/fill-static.png differ diff --git a/tests/targets/fisheye.png b/tests/targets/fisheye.png index a37222c..7609f3f 100644 Binary files a/tests/targets/fisheye.png and b/tests/targets/fisheye.png differ diff --git a/tests/targets/flip-horizontal.png b/tests/targets/flip-horizontal.png index 432046f..2052d8d 100644 Binary files a/tests/targets/flip-horizontal.png and b/tests/targets/flip-horizontal.png differ diff --git a/tests/targets/flip-vertical.png b/tests/targets/flip-vertical.png index 29262e3..0198d3c 100644 Binary files a/tests/targets/flip-vertical.png and b/tests/targets/flip-vertical.png differ diff --git a/tests/targets/font-1.png b/tests/targets/font-1.png index a3360ca..1d62f6f 100644 Binary files a/tests/targets/font-1.png and b/tests/targets/font-1.png differ diff --git a/tests/targets/font-2.png b/tests/targets/font-2.png index f4b5d1a..48664e9 100644 Binary files a/tests/targets/font-2.png and b/tests/targets/font-2.png differ diff --git a/tests/targets/font-3.png b/tests/targets/font-3.png index 8ca1c35..c8b4450 100644 Binary files a/tests/targets/font-3.png and b/tests/targets/font-3.png differ diff --git a/tests/targets/font-4.png b/tests/targets/font-4.png new file mode 100644 index 0000000..02dc4b7 Binary files /dev/null and b/tests/targets/font-4.png differ diff --git a/tests/targets/gif.gif b/tests/targets/gif.gif index e83dafe..6367e30 100644 Binary files a/tests/targets/gif.gif and b/tests/targets/gif.gif differ diff --git a/tests/targets/image.png b/tests/targets/image.png index b609b5a..2b60387 100644 Binary files a/tests/targets/image.png and b/tests/targets/image.png differ diff --git a/tests/targets/invert-hue.png b/tests/targets/invert-hue.png deleted file mode 100644 index b7e3729..0000000 Binary files a/tests/targets/invert-hue.png and /dev/null differ diff --git a/tests/targets/invert-hueshift.png b/tests/targets/invert-hueshift.png index dd72317..48a355c 100644 Binary files a/tests/targets/invert-hueshift.png and b/tests/targets/invert-hueshift.png differ diff --git a/tests/targets/invert-lightness.png b/tests/targets/invert-lightness.png new file mode 100644 index 0000000..d3fe73c Binary files /dev/null and b/tests/targets/invert-lightness.png differ diff --git a/tests/targets/invert-saturation.png b/tests/targets/invert-saturation.png deleted file mode 100644 index 0f4e6f3..0000000 Binary files a/tests/targets/invert-saturation.png and /dev/null differ diff --git a/tests/targets/invert-value.png b/tests/targets/invert-value.png index 33ab5f2..ee32989 100644 Binary files a/tests/targets/invert-value.png and b/tests/targets/invert-value.png differ diff --git a/tests/targets/invert.png b/tests/targets/invert.png index 748541b..48a355c 100644 Binary files a/tests/targets/invert.png and b/tests/targets/invert.png differ diff --git a/tests/targets/potato.png b/tests/targets/potato.png index eada661..5dcc95e 100644 Binary files a/tests/targets/potato.png and b/tests/targets/potato.png differ diff --git a/tests/targets/readme.png b/tests/targets/readme.png index 899b561..c01e1e7 100644 Binary files a/tests/targets/readme.png and b/tests/targets/readme.png differ diff --git a/tests/targets/resize.png b/tests/targets/resize.png index 2cabd4b..a7ab3b2 100644 Binary files a/tests/targets/resize.png and b/tests/targets/resize.png differ diff --git a/tests/targets/rotate-180.png b/tests/targets/rotate-180.png index c683335..01f3974 100644 Binary files a/tests/targets/rotate-180.png and b/tests/targets/rotate-180.png differ diff --git a/tests/targets/rotate-270.png b/tests/targets/rotate-270.png index e3e32f0..4b85e1a 100644 Binary files a/tests/targets/rotate-270.png and b/tests/targets/rotate-270.png differ diff --git a/tests/targets/rotate-45-noresize.png b/tests/targets/rotate-45-noresize.png index 0b58327..00840fa 100644 Binary files a/tests/targets/rotate-45-noresize.png and b/tests/targets/rotate-45-noresize.png differ diff --git a/tests/targets/rotate-45.png b/tests/targets/rotate-45.png index 2e63c5c..257925b 100644 Binary files a/tests/targets/rotate-45.png and b/tests/targets/rotate-45.png differ diff --git a/tests/targets/rotate-90.png b/tests/targets/rotate-90.png index 9754ef2..e872347 100644 Binary files a/tests/targets/rotate-90.png and b/tests/targets/rotate-90.png differ diff --git a/tests/targets/svg-features.png b/tests/targets/svg-features.png new file mode 100644 index 0000000..6c7340a Binary files /dev/null and b/tests/targets/svg-features.png differ diff --git a/tests/tiff.js b/tests/tiff.js new file mode 100644 index 0000000..0145a4c --- /dev/null +++ b/tests/tiff.js @@ -0,0 +1,32 @@ +const fs = require('fs').promises; +const crypto = require('crypto'); +const {Image, ImageType} = require('..'); + +const panic = message => { + console.error(message); + process.exit(1); +}; + +// sha256 of the expected RGBA output, generated with Pillow (16-bit samples use the high byte) +const expected = { + 'bw-tiff_deflate': 'bddeb89fcf5946ab3da243b483b3518f193ef3823cb22ec2f743a7315193d6fa', // 1-bit, deflate + 'cmyk-tiff_lzw-pred': '528738c07a1d81ee4b7e78cb90ca63da67b15a8f39f16715fc980729f3cdd2c9', // CMYK, LZW + horizontal predictor + 'f-tiff_adobe_deflate-pred': '16297996850e2361bf2d6e630efa25e25e44fc624880897e684979647caf906a', // 32-bit float, floating point predictor + 'i16b': 'ffbb919c37e84964bc8725db998b14a8ffde7c41bd2b26d700c73fcac5181c59', // big endian, 16-bit + 'la-tiff_lzw': '16297996850e2361bf2d6e630efa25e25e44fc624880897e684979647caf906a', // greyscale + alpha + 'p-packbits': '42a6b92f00c20dbd1ae352b9897a8f2318bd52aa0a895acb94e5d3a88f02d559', // palette, PackBits + 'rgb-bigtiff': '528738c07a1d81ee4b7e78cb90ca63da67b15a8f39f16715fc980729f3cdd2c9', // BigTIFF + 'rgb-jpeg': '4fc45173fe10c26acea8e1cb79fcabe3afeb66f48d3628498ffc4bfa6f414587', // JPEG compression + 'rgba-tiff_lzw-pred': '528738c07a1d81ee4b7e78cb90ca63da67b15a8f39f16715fc980729f3cdd2c9', // RGBA, LZW + horizontal predictor +}; + +(async () => { + for (const [name, hash] of Object.entries(expected)) { + const binary = await fs.readFile(`./tests/tiffs/${name}.tif`); + if (ImageType.getType(binary) !== 'tiff') panic(`${name}: not detected as TIFF`); + + const image = await Image.decode(binary); + if (image.width !== 157 || image.height !== 101) panic(`${name}: dimensions don't match`); + if (crypto.createHash('sha256').update(image.bitmap).digest('hex') !== hash) panic(`${name}: decoded pixels don't match`); + } +})(); diff --git a/tests/tiffs/README.md b/tests/tiffs/README.md new file mode 100644 index 0000000..a75f44a --- /dev/null +++ b/tests/tiffs/README.md @@ -0,0 +1 @@ +Generated from `tests/targets/external.png` with Pillow (libtiff) in different modes and compressions. diff --git a/tests/tiffs/bw-tiff_deflate.tif b/tests/tiffs/bw-tiff_deflate.tif new file mode 100644 index 0000000..b389946 Binary files /dev/null and b/tests/tiffs/bw-tiff_deflate.tif differ diff --git a/tests/tiffs/cmyk-tiff_lzw-pred.tif b/tests/tiffs/cmyk-tiff_lzw-pred.tif new file mode 100644 index 0000000..e883a49 Binary files /dev/null and b/tests/tiffs/cmyk-tiff_lzw-pred.tif differ diff --git a/tests/tiffs/f-tiff_adobe_deflate-pred.tif b/tests/tiffs/f-tiff_adobe_deflate-pred.tif new file mode 100644 index 0000000..4a12c8e Binary files /dev/null and b/tests/tiffs/f-tiff_adobe_deflate-pred.tif differ diff --git a/tests/tiffs/i16b.tif b/tests/tiffs/i16b.tif new file mode 100644 index 0000000..992f765 Binary files /dev/null and b/tests/tiffs/i16b.tif differ diff --git a/tests/tiffs/la-tiff_lzw.tif b/tests/tiffs/la-tiff_lzw.tif new file mode 100644 index 0000000..b5ab894 Binary files /dev/null and b/tests/tiffs/la-tiff_lzw.tif differ diff --git a/tests/tiffs/p-packbits.tif b/tests/tiffs/p-packbits.tif new file mode 100644 index 0000000..5b60c20 Binary files /dev/null and b/tests/tiffs/p-packbits.tif differ diff --git a/tests/tiffs/rgb-bigtiff.tif b/tests/tiffs/rgb-bigtiff.tif new file mode 100644 index 0000000..e835279 Binary files /dev/null and b/tests/tiffs/rgb-bigtiff.tif differ diff --git a/tests/tiffs/rgb-jpeg.tif b/tests/tiffs/rgb-jpeg.tif new file mode 100644 index 0000000..9210804 Binary files /dev/null and b/tests/tiffs/rgb-jpeg.tif differ diff --git a/tests/tiffs/rgba-tiff_lzw-pred.tif b/tests/tiffs/rgba-tiff_lzw-pred.tif new file mode 100644 index 0000000..ddef746 Binary files /dev/null and b/tests/tiffs/rgba-tiff_lzw-pred.tif differ diff --git a/tests/util/pixels.js b/tests/util/pixels.js new file mode 100644 index 0000000..6705f42 --- /dev/null +++ b/tests/util/pixels.js @@ -0,0 +1,7 @@ +const {Image} = require('../..'); + +// compares two encoded images by their pixels, so tests don't depend on the compressor the runtime provides +module.exports = async (a, b) => { + const [x, y] = await Promise.all([Image.decode(a), Image.decode(b)]); + return x.width === y.width && x.height === y.height && Buffer.from(x.bitmap).equals(Buffer.from(y.bitmap)); +}; diff --git a/tsconfig.json b/tsconfig.json index 1312e9a..16267c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,21 +1,28 @@ { "compilerOptions": { - "target": "es6", - "module": "commonjs", + "target": "es2022", + "lib": [ + "es2022" + ], + "types": [], + "module": "es2022", + "moduleResolution": "node", "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", - "allowJs": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "lib": ["es6"] + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "rootDir": "src", + "outDir": "dist", + "noEmitOnError": true, + "rewriteRelativeImportExtensions": true, + "allowImportingTsExtensions": true, + "noImplicitOverride": true, + "stripInternal": true }, - "files": ["ImageScript.d.ts"] + "include": [ + "src" + ] } diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..7cbc31a --- /dev/null +++ b/typedoc.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": [ + "src/index.ts" + ], + "out": "docs", + "name": "ImageScript", + "readme": "README.md", + "includeVersion": true, + "excludePrivate": true, + "excludeInternal": true, + "sort": [ + "kind", + "alphabetical" + ], + "githubPages": true, + "hostedBaseUrl": "https://imagescript.matmen.dev/", + "gitRevision": "master", + "navigationLinks": { + "GitHub": "https://github.com/matmen/ImageScript", + "Changelog": "https://github.com/matmen/ImageScript/blob/master/CHANGELOG.md", + "Discord": "https://discord.gg/8hPrwAH" + }, + "validation": { + "notExported": true, + "invalidLink": true, + "notDocumented": true + }, + "excludeExternals": true +} diff --git a/utils/mem.js b/utils/mem.js deleted file mode 100644 index 4a97f47..0000000 --- a/utils/mem.js +++ /dev/null @@ -1,26 +0,0 @@ -function view(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} - -function from_parts(buffers, shared = false) { - let length = 0; - let offset = 0; - buffers.forEach(buffer => length += (null == buffer.byteLength ? buffer.length : buffer.byteLength)); - - const u8 = new Uint8Array(shared ? new SharedArrayBuffer(length) : length); - - buffers.forEach(buffer => { - const ref = Array.isArray(buffer) ? buffer : view(buffer, true); - - u8.set(ref, offset); - offset += ref.length; - }); - - return u8; -} - -module.exports = { view, from_parts }; \ No newline at end of file diff --git a/v2/codecs/magic.js b/v2/codecs/magic.js deleted file mode 100644 index a31f6ca..0000000 --- a/v2/codecs/magic.js +++ /dev/null @@ -1,25 +0,0 @@ -const { view } = require('../../utils/mem.js'); - -const formats = { - ttf: { type: 'font', format: 'ttf' }, - otf: { type: 'font', format: 'otf' }, - svg: { type: 'image', format: 'svg' }, - png: { type: 'image', format: 'png' }, - gif: { type: 'image', format: 'gif' }, - jpeg: { type: 'image', format: 'jpeg' }, - tiff: { type: 'image', format: 'tiff' }, -}; - -function buffer(init) { - const u8 = view(init); - if (0 === u8.length) return; - if (0xff === u8[0] && 0xd8 === u8[1] && 0xff === u8[2]) return formats.jpeg; - if (0x4d === u8[0] && 0x4d === u8[1] && 0x00 === u8[2] && 0x2a === u8[3]) return formats.tiff; - if (0x49 === u8[0] && 0x49 === u8[1] && 0x2a === u8[2] && 0x00 === u8[3]) return formats.tiff; - if (0x00 === u8[0] && 0x01 === u8[1] && 0x00 === u8[2] && 0x00 === u8[3] && 0x00 === u8[4]) return formats.ttf; - if (0x4F === u8[0] && 0x54 === u8[1] && 0x54 === u8[2] && 0x4F === u8[3] && 0x00 === u8[4]) return formats.otf; - if (0x47 === u8[0] && 0x49 === u8[1] && 0x46 === u8[2] && 0x38 === u8[3] && 0x61 === u8[5] && (0x37 === u8[4] || 0x39 === u8[4])) return formats.gif; - if (0x89 === u8[0] && 0x50 === u8[1] && 0x4e === u8[2] && 0x47 === u8[3] && 0x0d === u8[4] && 0x0a === u8[5] && 0x1a === u8[6] && 0x0a === u8[7]) return formats.png; -} - -module.exports = { buffer }; \ No newline at end of file diff --git a/v2/framebuffer.d.ts b/v2/framebuffer.d.ts deleted file mode 100644 index b66a01b..0000000 --- a/v2/framebuffer.d.ts +++ /dev/null @@ -1,67 +0,0 @@ -type rgb = [r: number, g: number, b: number]; -type rgba = [r: number, g: number, b: number, a: number]; - -export class Color { - private value: number; - - constructor(color: string); - static rgb(r: number, g: number, b: number): number; - static rgba(r: number, g: number, b: number, a: number): number; - static hsla(h: number, s: number, l: number, a: number): number; - - get rgb(): rgb; - toJSON(): number; - get rgba(): rgba; - valueOf(): number; - get name(): null | string; - toString(radix?: '16' | 'hex' | 'rgb' | 'rgba'): string; -} - -export default class framebuffer { - readonly width: number; - readonly u8: Uint8Array; - readonly height: number; - readonly u32: Uint32Array; - private readonly view: DataView; - - constructor(width: number, height: number, buffer?: BufferSource); - static from(framebuffer: { width: number, height: number, u8?: BufferSource, buffer?: BufferSource }): framebuffer; - - toString(): string; - clone(): framebuffer; - get(x: number, y: number): number; - at(x: number, y: number): Uint8Array; - flip(type: 'vertical' | 'horizontal'): this; - rotate(deg: number, resize?: boolean): this; - set(x: number, y: number, color: number): void; - overlay(frame: this, x?: number, y?: number): this; - replace(frame: this, x?: number, y?: number): this; - toJSON(): { width: number, height: number, buffer: number[] }; - static decode(format: 'png', buffer: BufferSource): framebuffer; - scale(type: 'cubic' | 'linear' | 'nearest', factor: number): this; - [Symbol.iterator](): Generator<[x: number, y: number], [x: number, y: number]>; - resize(type: 'cubic' | 'linear' | 'nearest', width: number, height: number): this; - encode(format: 'png', options?: { compression?: 'none' | 'fast' | 'best' | 'default' }): Uint8Array; - - cut(type: 'circle', feathering?: number): this; - cut(type: 'box', x: number, y: number, width: number, height: number): this; - - crop(type: 'circle', feathering?: number): this; - crop(type: 'box', x: number, y: number, width: number, height: number): this; - - blur(type: 'cubic'): this; - blur(type: 'box', radius: number): this; - blur(type: 'gaussian', radius: number): this; - - swap(old: rgba, color: rgba): this; - swap(old: Color, color: Color): this; - swap(old: number, color: number): this; - - fill(rgba: rgba): this; - fill(color: Color): this; - fill(color: number): this; - fill(cb: (x: number, y: number) => number): this; - - pixels(type?: 'int'): Generator<[x: number, y: number, color: number], [x: number, y: number, color: number]>; - pixels(type: 'rgba'): Generator<[x: number, y: number, rgba: Uint8Array], [x: number, y: number, rgba: Uint8Array]>; -} \ No newline at end of file diff --git a/v2/framebuffer.js b/v2/framebuffer.js deleted file mode 100644 index bd8db09..0000000 --- a/v2/framebuffer.js +++ /dev/null @@ -1,2352 +0,0 @@ -var __defProp = Object.defineProperty; -var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); -var __export = (target, all) => { - __markAsModule(target); - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; - -// v2/framebuffer.mjs -__export(exports, { - Color: () => color, - default: () => framebuffer -}); - -// v2/ops/color.js -var color_exports = {}; -__export(color_exports, { - blend: () => blend, - default: () => color, - from_rgba: () => from_rgba, - parse: () => parse, - to_rgba: () => to_rgba -}); -var short_hex_regex = /^#?([\da-f]{3,4})$/; -var long_hex_regex = /^#?((?:[\da-f]{2}){3,4})$/; -var rgb_regex = /^rgba?\((?(?:\d*\.)?\d+)(?: +| *, *)(?(?:\d*\.)?\d+)(?: +| *, *)(?(?:\d*\.)?\d+)(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; -var rgb_percentage_regex = /^rgba?\((?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; -var hsl_regex = /^hsla?\((?(?:\d*\.)?\d+)(?|deg|rad|grad|turn)(?: +| *, *)(?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; -function clamp(min, max2, int) { - return Math.min(Math.max(Math.round(int), min), max2); -} -function to_rgba(int) { - return [int >> 24 & 255, int >> 16 & 255, int >> 8 & 255, int & 255]; -} -function from_rgba(r, g, b, a) { - return (r & 255) << 24 | (g & 255) << 16 | (b & 255) << 8 | a & 255; -} -function parse_alpha(a) { - return clamp(0, 255, a[a.length - 1] === "%" ? 255 / 100 * parseFloat(a) : +a * 255); -} -function hue_from_type(h, t) { - if (t === "turn") - return h / 1; - if (t === "grad") - return h / 400; - if (!t || t === "deg") - return h / 360; - if (t === "rad") - return h / (2 * Math.PI); -} -function hue2rgb(p, q, t) { - if (t < 0) - t += 1; - else if (t > 1) - t -= 1; - if (t < 1 / 2) - return q; - if (t < 1 / 6) - return p + 6 * t * (q - p); - if (t < 2 / 3) - return p + 6 * (q - p) * (2 / 3 - t); - return p; -} -function blend(fg, bg) { - const fa = fg & 255; - const alpha = fa + 1 | 0; - const inv_alpha = 256 - fa | 0; - const r = alpha * (fg >>> 24) + inv_alpha * (bg >>> 24) >> 8; - const b = alpha * (fg >> 8 & 255) + inv_alpha * (bg >> 8 & 255) >> 8; - const g = alpha * (fg >> 16 & 255) + inv_alpha * (bg >> 16 & 255) >> 8; - return (r & 255) << 24 | (g & 255) << 16 | (b & 255) << 8 | Math.max(fa, bg & 255) & 255; -} -function parse(any) { - let x2 = null; - if ((x2 = colors.get(any)) !== void 0) - return x2; - if (x2 = long_hex_regex.exec(any)) - return parseInt(`${x2[1]}${x2[1].length === 8 ? "" : "ff"}`, 16); - if (x2 = hsl_regex.exec(any)) - return color.hsla(hue_from_type(x2[1], x2[2]), x2[3] / 100, x2[4] / 100, x2[5] ? 1 / 255 * parse_alpha(x2[5]) : 1); - if (x2 = rgb_regex.exec(any)) - return color.rgba(clamp(0, 255, +x2[1]), clamp(0, 255, +x2[2]), clamp(0, 255, +x2[3]), x2[4] ? parse_alpha(x2[4]) : 255); - if (x2 = short_hex_regex.exec(any)) - return parseInt(`${x2[1][0]}${x2[1][0]}${x2[1][1]}${x2[1][1]}${x2[1][2]}${x2[1][2]}${x2[1].length === 3 ? "ff" : `${x2[1][3]}${x2[1][3]}`}`, 16); - if (x2 = rgb_percentage_regex.exec(any)) - return color.rgba(clamp(0, 255, +x2[1] * (255 / 100)), clamp(0, 255, +x2[2] * (255 / 100)), clamp(0, 255, +x2[3] * (255 / 100)), x2[4] ? parse_alpha(x2[4]) : 255); - return null; -} -var color = class { - constructor(any) { - this.value = parse(String(any).toLowerCase()); - if (this.value === null) - throw new Error(`invalid css color (${any})`); - } - static rgb(r, g, b) { - return this.rgba(r, g, b, 255); - } - static rgba(r, g, b, a) { - return (r & 255) << 24 | (g & 255) << 16 | (b & 255) << 8 | a & 255; - } - static hsla(h, s, l, a) { - s = Math.min(1, Math.max(0, s)); - a = Math.min(1, Math.max(0, a)); - if (s === 0) - return this.rgba(255, 255, 255, a * 255); - h %= 1; - l = Math.min(1, Math.max(0, l)); - const q = l < 0.5 ? l + s * l : l + s - l * s; - const p = 2 * l - q; - const g = hue2rgb(p, q, h); - const r = hue2rgb(p, q, h + 1 / 3); - const b = hue2rgb(p, q, h - 1 / 3); - return this.rgba(r * 255, g * 355, b * 255, a * 255); - } - valueOf() { - return this.value; - } - toJSON() { - return this.value >>> 0; - } - get rgb() { - return [this.value >>> 24, this.value >> 16 & 255, this.value >> 8 & 255]; - } - get rgba() { - return [this.value >>> 24, this.value >> 16 & 255, this.value >> 8 & 255, this.value & 255]; - } - get name() { - for (const color3 of colors.keys()) { - if (this.value === colors.get(color3)) - return color3; - } - return null; - } - toString(radix) { - const type = String(radix).toLowerCase(); - const rgba2 = this.rgba; - if (type === "rgb" || type === "rgba") - return `rgb${type[3] ? "a" : ""}(${rgba2[0]}, ${rgba2[1]}, ${rgba2[2]}${type[3] ? `, ${clamp(0, 100, 100 / 255 * rgba2[3])}%` : ""})`; - if (type === "16" || type === "hex") - return `#${rgba2[0].toString(16).padStart(2, "0")}${rgba2[1].toString(16).padStart(2, "0")}${rgba2[2].toString(16).padStart(2, "0")}${rgba2[3] === 255 ? "" : rgba2[3].toString(16).padStart(2, "0")}`; - return this.value.toString(); - } -}; -var colors = new Map([ - ["aliceblue", 4042850303], - ["antiquewhite", 4209760255], - ["aqua", 16777215], - ["aquamarine", 2147472639], - ["azure", 4043309055], - ["beige", 4126530815], - ["bisque", 4293182719], - ["black", 255], - ["blanchedalmond", 4293643775], - ["blue", 65535], - ["blueviolet", 2318131967], - ["brown", 2771004159], - ["burlywood", 3736635391], - ["cadetblue", 1604231423], - ["chartreuse", 2147418367], - ["chocolate", 3530104575], - ["coral", 4286533887], - ["cornflowerblue", 1687547391], - ["cornsilk", 4294499583], - ["crimson", 3692313855], - ["cyan", 16777215], - ["darkblue", 35839], - ["darkcyan", 9145343], - ["darkgoldenrod", 3095792639], - ["darkgray", 2846468607], - ["darkgreen", 6553855], - ["darkgrey", 2846468607], - ["darkkhaki", 3182914559], - ["darkmagenta", 2332068863], - ["darkolivegreen", 1433087999], - ["darkorange", 4287365375], - ["darkorchid", 2570243327], - ["darkred", 2332033279], - ["darksalmon", 3918953215], - ["darkseagreen", 2411499519], - ["darkslateblue", 1211993087], - ["darkslategray", 793726975], - ["darkslategrey", 793726975], - ["darkturquoise", 13554175], - ["darkviolet", 2483082239], - ["deeppink", 4279538687], - ["deepskyblue", 12582911], - ["dimgray", 1768516095], - ["dimgrey", 1768516095], - ["dodgerblue", 512819199], - ["firebrick", 2988581631], - ["floralwhite", 4294635775], - ["forestgreen", 579543807], - ["fuchsia", 4278255615], - ["gainsboro", 3705462015], - ["ghostwhite", 4177068031], - ["gold", 4292280575], - ["goldenrod", 3668254975], - ["gray", 2155905279], - ["green", 8388863], - ["greenyellow", 2919182335], - ["grey", 2155905279], - ["honeydew", 4043305215], - ["hotpink", 4285117695], - ["indianred", 3445382399], - ["indigo", 1258324735], - ["ivory", 4294963455], - ["khaki", 4041641215], - ["lavender", 3873897215], - ["lavenderblush", 4293981695], - ["lawngreen", 2096890111], - ["lemonchiffon", 4294626815], - ["lightblue", 2916673279], - ["lightcoral", 4034953471], - ["lightcyan", 3774873599], - ["lightgoldenrodyellow", 4210742015], - ["lightgray", 3553874943], - ["lightgreen", 2431553791], - ["lightgrey", 3553874943], - ["lightpink", 4290167295], - ["lightsalmon", 4288707327], - ["lightseagreen", 548580095], - ["lightskyblue", 2278488831], - ["lightslategray", 2005441023], - ["lightslategrey", 2005441023], - ["lightsteelblue", 2965692159], - ["lightyellow", 4294959359], - ["lime", 16711935], - ["limegreen", 852308735], - ["linen", 4210091775], - ["magenta", 4278255615], - ["maroon", 2147483903], - ["mediumaquamarine", 1724754687], - ["mediumblue", 52735], - ["mediumorchid", 3126187007], - ["mediumpurple", 2473647103], - ["mediumseagreen", 1018393087], - ["mediumslateblue", 2070474495], - ["mediumspringgreen", 16423679], - ["mediumturquoise", 1221709055], - ["mediumvioletred", 3340076543], - ["midnightblue", 421097727], - ["mintcream", 4127193855], - ["mistyrose", 4293190143], - ["moccasin", 4293178879], - ["navajowhite", 4292783615], - ["navy", 33023], - ["oldlace", 4260751103], - ["olive", 2155872511], - ["olivedrab", 1804477439], - ["orange", 4289003775], - ["orangered", 4282712319], - ["orchid", 3664828159], - ["palegoldenrod", 4008225535], - ["palegreen", 2566625535], - ["paleturquoise", 2951671551], - ["palevioletred", 3681588223], - ["papayawhip", 4293907967], - ["peachpuff", 4292524543], - ["peru", 3448061951], - ["pink", 4290825215], - ["plum", 3718307327], - ["powderblue", 2967529215], - ["purple", 2147516671], - ["rebeccapurple", 1714657791], - ["red", 4278190335], - ["rosybrown", 3163525119], - ["royalblue", 1097458175], - ["saddlebrown", 2336560127], - ["salmon", 4202722047], - ["sandybrown", 4104413439], - ["seagreen", 780883967], - ["seashell", 4294307583], - ["sienna", 2689740287], - ["silver", 3233857791], - ["skyblue", 2278484991], - ["slateblue", 1784335871], - ["slategray", 1887473919], - ["slategrey", 1887473919], - ["snow", 4294638335], - ["springgreen", 16744447], - ["steelblue", 1182971135], - ["tan", 3535047935], - ["teal", 8421631], - ["thistle", 3636451583], - ["tomato", 4284696575], - ["transparent", 0], - ["turquoise", 1088475391], - ["violet", 4001558271], - ["wheat", 4125012991], - ["white", 4294967295], - ["whitesmoke", 4126537215], - ["yellow", 4294902015], - ["yellowgreen", 2597139199] -]); - -// v2/util/mem.js -function view(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) - return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) - return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} - -// v2/ops/flip.js -var flip_exports = {}; -__export(flip_exports, { - horizontal: () => horizontal, - vertical: () => vertical -}); -function horizontal(framebuffer2) { - let offset = 0 | 0; - const u323 = framebuffer2.u32; - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - for (let y = 0 | 0; y < height; y++) { - u323.subarray(offset, offset += width).reverse(); - } -} -function vertical(framebuffer2) { - const u323 = framebuffer2.u32; - const width = framebuffer2.width | 0; - const oheight = framebuffer2.height | 0; - const height = framebuffer2.height / 2 | 0; - for (let y = 0 | 0; y < height; y++) { - const yo = y * width | 0; - const wo1y = width * (oheight - 1 - y) | 0; - for (let x2 = 0 | 0; x2 < width; x2++) { - const offset = x2 + yo; - const offset2 = x2 + wo1y; - const top = u323[offset]; - const bottom = u323[offset2]; - u323[offset2] = top; - u323[offset] = bottom; - } - } -} - -// v2/ops/fill.js -var fill_exports = {}; -__export(fill_exports, { - color: () => color2, - fn: () => fn, - swap: () => swap -}); -function color2(int, framebuffer2) { - framebuffer2.view.setUint32(0, int); - framebuffer2.u32.fill(framebuffer2.u32[0]); -} -function fn(cb, framebuffer2) { - let offset = 0 | 0; - const view3 = framebuffer2.view; - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - for (let y = 1 | 0; y <= height; y++) { - for (let x2 = 1 | 0; x2 <= width; x2++) { - view3.setUint32(offset, cb(x2, y), false); - offset += 4; - } - } -} -function swap(old, int, framebuffer2) { - { - const t = new Uint32Array(2); - const v = new DataView(t.buffer); - old = (v.setUint32(0, old), t[0]); - int = (v.setUint32(4, int), t[1]); - } - const u323 = framebuffer2.u32; - const l = framebuffer2.u32.length | 0; - for (let o = 0 | 0; o < l; o++) { - if (old === u323[o]) - u323[o] = int; - } -} - -// v2/ops/blur.js -var blur_exports = {}; -__export(blur_exports, { - box: () => box, - cubic: () => cubic, - gaussian: () => gaussian -}); -function cubic(framebuffer2) { - const width = framebuffer2.width; - const height = framebuffer2.height; - framebuffer2.resize("cubic", Math.max(1, 8e-3 * width), Math.max(1, 8e-3 * height)); - framebuffer2.resize("cubic", width, height); -} -function box(radius, framebuffer2) { - if (!radius) - return; - const u82 = framebuffer2.u8; - const width = framebuffer2.width; - const height = framebuffer2.height; - const old = framebuffer2.u8.slice(); - bb(u82, old, width, height, radius); -} -function gaussian(radius, framebuffer2) { - if (0 >= radius) - return; - const a = Math.exp(0.726 ** 2) / radius; - const g1 = Math.exp(-a); - const g2 = Math.exp(a * -2); - const old = framebuffer2.u8.slice(); - const k = (1 - g1) ** 2 / (1 - g2 + 2 * a * g1); - const b2 = -g2; - const b1 = 2 * g1; - const a3 = -k * g2; - const a1 = k * g1 * (a - 1); - const a2 = k * g1 * (a + 1); - const lc2 = (k + a1) / (1 - b1 - b2); - const width = framebuffer2.width | 0; - const rc = (a2 + a3) / (1 - b1 - b2); - const height = framebuffer2.height | 0; - const tmp = new Float32Array(4 * Math.max(width, height)); - gc(old, framebuffer2.u8, tmp, width, height, k, a1, a2, a3, b1, b2, lc2, rc); - gc(framebuffer2.u8, old, tmp, height, width, k, a1, a2, a3, b1, b2, lc2, rc); -} -function bb(u82, old, width, height, radius) { - const divisor = 1 / (1 + radius + radius); - bbt(u82, old, width, height, radius, divisor); - bbh(u82, old, width, height, radius, divisor); -} -function bbh(u82, old, width, height, radius, divisor) { - for (var y = 0; y < height; y++) { - let y_offset = y * width; - let li = y_offset; - let ri = radius + y_offset; - const fv_offset = 4 * y_offset; - const lv_offset = 4 * (width - 1 + y_offset); - const rfv = old[fv_offset]; - const gfv = old[1 + fv_offset]; - const bfv = old[2 + fv_offset]; - const afv = old[3 + fv_offset]; - const rlv = old[lv_offset]; - const glv = old[1 + lv_offset]; - const blv = old[2 + lv_offset]; - const alv = old[3 + lv_offset]; - let r = rfv * (1 + radius); - let g = gfv * (1 + radius); - let b = bfv * (1 + radius); - let a = afv * (1 + radius); - for (let x2 = 0; x2 < radius; x2++) { - const offset = 4 * (x2 + y_offset); - r += old[offset]; - g += old[1 + offset]; - b += old[2 + offset]; - a += old[3 + offset]; - } - for (let x2 = 0; x2 <= radius; x2++) { - let offset = 4 * ri++; - r += old[offset] - rfv; - g += old[1 + offset] - gfv; - b += old[2 + offset] - bfv; - a += old[3 + offset] - afv; - offset = 4 * y_offset++; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - } - for (let x2 = 1 + radius; x2 < width - radius; x2++) { - let offset = 4 * ri++; - const roffset = 4 * li++; - r += old[offset] - old[roffset]; - g += old[1 + offset] - old[1 + roffset]; - b += old[2 + offset] - old[2 + roffset]; - a += old[3 + offset] - old[3 + roffset]; - offset = 4 * y_offset++; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - } - for (let x2 = width - radius; x2 < width; x2++) { - let offset = 4 * li++; - r += rlv - old[offset]; - g += glv - old[1 + offset]; - b += blv - old[2 + offset]; - a += alv - old[3 + offset]; - offset = 4 * y_offset++; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - } - } -} -function bbt(u82, old, width, height, radius, divisor) { - for (var x2 = 0; x2 < width; x2++) { - let ti = x2; - let li = ti; - const fv_offset = 4 * ti; - let ri = ti + width * radius; - const lv_offset = 4 * (ti + width * (height - 1)); - const rfv = old[fv_offset]; - const gfv = old[1 + fv_offset]; - const bfv = old[2 + fv_offset]; - const afv = old[3 + fv_offset]; - const rlv = old[lv_offset]; - const glv = old[1 + lv_offset]; - const blv = old[2 + lv_offset]; - const alv = old[3 + lv_offset]; - let r = rfv * (1 + radius); - let g = gfv * (1 + radius); - let b = bfv * (1 + radius); - let a = afv * (1 + radius); - for (let y = 0; y < radius; y++) { - const offset = 4 * (ti + y * width); - r += old[offset]; - g += old[1 + offset]; - b += old[2 + offset]; - a += old[3 + offset]; - } - for (let y = 0; y <= radius; y++) { - let offset = 4 * ri; - r += old[offset] - rfv; - g += old[1 + offset] - gfv; - b += old[2 + offset] - bfv; - a += old[3 + offset] - afv; - offset = 4 * ti; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - ri += width; - ti += width; - } - for (let y = 1 + radius; y < height - radius; y++) { - let offset = 4 * ri; - const xoffset = 4 * li; - r += old[offset] - old[xoffset]; - g += old[1 + offset] - old[1 + xoffset]; - b += old[2 + offset] - old[2 + xoffset]; - a += old[3 + offset] - old[3 + xoffset]; - offset = 4 * ti; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - li += width; - ri += width; - ti += width; - } - for (let y = height - radius; y < height; y++) { - let offset = 4 * li; - r += rlv - old[offset]; - g += glv - old[1 + offset]; - b += blv - old[2 + offset]; - a += alv - old[3 + offset]; - offset = 4 * ti; - u82[offset] = Math.round(r * divisor); - u82[1 + offset] = Math.round(g * divisor); - u82[2 + offset] = Math.round(b * divisor); - u82[3 + offset] = Math.round(a * divisor); - li += width; - ti += width; - } - } -} -function gc(u82, old, tmp, width, height, k, a1, a2, a3, b1, b2, lc2, rc) { - const width4 = width * 4; - const height4 = height * 4; - const hw1 = height * (width - 1); - for (let y = 0; y < height; y++) { - let toffset = 0 | 0; - let ooffset = y * width4 | 0; - let offset = 4 * (y + hw1) | 0; - let por = old[ooffset]; - let pog = old[1 + ooffset]; - let pob = old[2 + ooffset]; - let poa = old[3 + ooffset]; - let pur = lc2 * por; - let pug = lc2 * pog; - let pub = lc2 * pob; - let pua = lc2 * poa; - let ppur = pur; - let ppug = pug; - let ppub = pub; - let ppua = pua; - for (let x2 = 0; x2 < width; x2++) { - const cor2 = old[ooffset++]; - const cog2 = old[ooffset++]; - const cob2 = old[ooffset++]; - const coa2 = old[ooffset++]; - const cur = k * cor2 + a1 * por + b1 * pur + b2 * ppur; - const cug = k * cog2 + a1 * pog + b1 * pug + b2 * ppug; - const cub = k * cob2 + a1 * pob + b1 * pub + b2 * ppub; - const cua = k * coa2 + a1 * poa + b1 * pua + b2 * ppua; - ppur = pur; - pur = cur; - por = cor2; - ppug = pug; - pug = cug; - pog = cog2; - ppub = pub; - pub = cub; - pob = cob2; - ppua = pua; - pua = cua; - poa = coa2; - tmp[toffset++] = pur; - tmp[toffset++] = pug; - tmp[toffset++] = pub; - tmp[toffset++] = pua; - } - ooffset -= 4; - toffset -= 4; - ppur = rc * (por = old[ooffset]); - ppug = rc * (pog = old[1 + ooffset]); - ppub = rc * (pob = old[2 + ooffset]); - ppua = rc * (poa = old[3 + ooffset]); - pur = ppur; - pug = ppug; - pub = ppub; - pua = ppua; - let cor = por; - let cog = pog; - let cob = pob; - let coa = poa; - for (let x2 = width - 1; 0 <= x2; x2--) { - const cur = a2 * cor + a3 * por + b1 * pur + b2 * ppur; - const cug = a2 * cog + a3 * pog + b1 * pug + b2 * ppug; - const cub = a2 * cob + a3 * pob + b1 * pub + b2 * ppub; - const cua = a2 * coa + a3 * poa + b1 * pua + b2 * ppua; - ppur = pur; - pur = cur; - por = cor; - ppug = pug; - pug = cug; - pog = cog; - ppub = pub; - pub = cub; - pob = cob; - ppua = pua; - pua = cua; - poa = coa; - cor = old[ooffset]; - cog = old[1 + ooffset]; - cob = old[2 + ooffset]; - coa = old[3 + ooffset]; - u82[offset] = pur + tmp[toffset]; - u82[1 + offset] = pug + tmp[1 + toffset]; - u82[2 + offset] = pub + tmp[2 + toffset]; - u82[3 + offset] = pua + tmp[3 + toffset]; - ooffset -= 4; - toffset -= 4; - offset -= height4; - } - } -} - -// v2/ops/crop.js -var crop_exports = {}; -__export(crop_exports, { - circle: () => circle, - crop: () => crop, - cut: () => cut -}); -function clamp2(min, int, max2) { - const t = int < min ? min : int; - return t > max2 ? max2 : t; -} -function cut(x2, y, width, height, framebuffer2) { - width |= 0; - height |= 0; - const frame = new framebuffer2.constructor(width, height); - const n32 = frame.u32; - const o32 = framebuffer2.u32; - const fwidth = framebuffer2.width | 0; - for (let yy = 0 | 0; yy < height; yy++) { - const offset = x2 + fwidth * (y + yy); - n32.set(o32.subarray(offset, width + offset), yy * width); - } - return frame; -} -function crop(x2, y, width, height, framebuffer2) { - width |= 0; - height |= 0; - const old = framebuffer2.u32; - const fwidth = framebuffer2.width | 0; - const u323 = framebuffer2.u32 = new Uint32Array(width * height); - for (let yy = 0; yy < height; yy++) { - const offset = x2 + fwidth * (y + yy); - u323.set(old.subarray(offset, width + offset), yy * width); - } - framebuffer2.width = width; - framebuffer2.height = height; - framebuffer2.u8 = new Uint8Array(framebuffer2.u32.buffer); - framebuffer2.view = new DataView(framebuffer2.u32.buffer); -} -function circle(feathering, framebuffer2) { - const u82 = framebuffer2.u8; - const u323 = framebuffer2.u32; - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - const rad = Math.min(width, height) / 2; - const cx = width / 2; - const cy = height / 2; - const rad_2 = rad ** 2; - const feathering_12 = feathering ** (1 / 2); - for (let y = 0 | 0; y < height; y++) { - const cdy = (y - cy) ** 2; - const y_offset = y * width; - for (let x2 = 0 | 0; x2 < width; x2++) { - const cd = cdy + (x2 - cx) ** 2; - if (cd > rad_2) - u323[x2 + y_offset] = 0; - else if (feathering) - u82[3 + 4 * (x2 + y_offset)] *= clamp2(0, 1 - cd / rad_2 * feathering_12, 1); - } - } - return framebuffer2; -} - -// v2/ops/resize.js -var resize_exports = {}; -__export(resize_exports, { - cubic: () => cubic2, - linear: () => linear, - nearest: () => nearest -}); -function lerp(a, b, t) { - return t * b + a * (1 - t); -} -function clamp3(min, int, max2) { - const t = int < min ? min : int; - return t > max2 ? max2 : t; -} -function clamped(x2, y, width, height) { - return 4 * (clamp3(0, x2, width - 1) + clamp3(0, y, height - 1) * width); -} -function hermite(A, B, C, D, t) { - const c = C / 2 + A / -2; - const b = A + C * 2 - D / 2 - B * 2.5; - const a = D / 2 + A / -2 + B * 1.5 - C * 1.5; - const t2 = t * t; - return B + c * t + b * t2 + a * t * t2; -} -function nearest(width, height, framebuffer2) { - width = width | 0; - height = height | 0; - const old = framebuffer2.u32; - const fwidth = framebuffer2.width | 0; - const fheight = framebuffer2.height | 0; - const u323 = framebuffer2.u32 = new Uint32Array(width * height); - const dw = 1 / width; - const dh = 1 / height; - const xw = dw * fwidth; - const yw = dh * fheight; - for (let y = 0 | 0; y < height; y++) { - const yoffset = y * width; - const yyoffset = fwidth * (y * yw | 0); - for (let x2 = 0 | 0; x2 < width; x2++) { - u323[x2 + yoffset] = old[yyoffset + (x2 * xw | 0)]; - } - } - framebuffer2.width = width; - framebuffer2.height = height; - framebuffer2.u8 = new Uint8Array(u323.buffer); - framebuffer2.view = new DataView(u323.buffer); -} -function linear(width, height, framebuffer2) { - width = width | 0; - height = height | 0; - const old = framebuffer2.u8; - const old_width = framebuffer2.width | 0; - const old_height = framebuffer2.height | 0; - const u82 = new Uint8ClampedArray(4 * width * height); - let offset = 0 | 0; - const width1 = 1 / (width - 1); - const height1 = 1 / (height - 1); - for (let y = 0 | 0; y < height; y++) { - const yy = old_height * (y * height1) - 0.5; - const yyi = yy | 0; - const ty = yy - yyi; - for (let x2 = 0 | 0; x2 < width; x2++) { - const xx = old_width * (x2 * width1) - 0.5; - const xxi = xx | 0; - const tx = xx - xxi; - const s0 = clamped(xxi, yyi, old_width, old_height); - const s1 = clamped(1 + xxi, yyi, old_width, old_height); - const s2 = clamped(xxi, 1 + yyi, old_width, old_height); - const s3 = clamped(1 + xxi, 1 + yyi, old_width, old_height); - u82[offset++] = lerp(lerp(old[s0], old[s2], tx), lerp(old[s1], old[s3], tx), ty); - u82[offset++] = lerp(lerp(old[1 + s0], old[1 + s2], tx), lerp(old[1 + s1], old[1 + s3], tx), ty); - u82[offset++] = lerp(lerp(old[2 + s0], old[2 + s2], tx), lerp(old[2 + s1], old[2 + s3], tx), ty); - u82[offset++] = lerp(lerp(old[3 + s0], old[3 + s2], tx), lerp(old[3 + s1], old[3 + s3], tx), ty); - } - } - framebuffer2.width = width; - framebuffer2.height = height; - framebuffer2.u8 = new Uint8Array(u82.buffer); - framebuffer2.view = new DataView(u82.buffer); - framebuffer2.u32 = new Uint32Array(u82.buffer); -} -function cubic2(width, height, framebuffer2) { - width = width | 0; - height = height | 0; - const old = framebuffer2.u8; - const old_width = framebuffer2.width | 0; - const old_height = framebuffer2.height | 0; - const u82 = new Uint8ClampedArray(4 * width * height); - let offset = 0 | 0; - const width1 = 1 / (width - 1); - const height1 = 1 / (height - 1); - for (let y = 0 | 0; y < height; y++) { - const yy = old_height * (y * height1) - 0.5; - const yyi = yy | 0; - const ty = yy - yyi; - for (let x2 = 0 | 0; x2 < width; x2++) { - const xx = old_width * (x2 * width1) - 0.5; - const xxi = xx | 0; - const tx = xx - xxi; - const s0 = clamped(xxi - 1, yyi - 1, old_width, old_height); - const s1 = clamped(0 + xxi, yyi - 1, old_width, old_height); - const s2 = clamped(1 + xxi, yyi - 1, old_width, old_height); - const s3 = clamped(2 + xxi, yyi - 1, old_width, old_height); - const s4 = clamped(xxi - 1, yyi, old_width, old_height); - const s5 = clamped(0 + xxi, yyi, old_width, old_height); - const s6 = clamped(1 + xxi, yyi, old_width, old_height); - const s7 = clamped(2 + xxi, yyi, old_width, old_height); - const s8 = clamped(xxi - 1, 1 + yyi, old_width, old_height); - const s9 = clamped(0 + xxi, 1 + yyi, old_width, old_height); - const s10 = clamped(1 + xxi, 1 + yyi, old_width, old_height); - const s11 = clamped(2 + xxi, 1 + yyi, old_width, old_height); - const s12 = clamped(xxi - 1, 2 + yyi, old_width, old_height); - const s13 = clamped(0 + xxi, 2 + yyi, old_width, old_height); - const s14 = clamped(1 + xxi, 2 + yyi, old_width, old_height); - const s15 = clamped(2 + xxi, 2 + yyi, old_width, old_height); - const c0 = hermite(old[s0], old[s1], old[s2], old[s3], tx); - const c00 = hermite(old[1 + s0], old[1 + s1], old[1 + s2], old[1 + s3], tx); - const c000 = hermite(old[2 + s0], old[2 + s1], old[2 + s2], old[2 + s3], tx); - const c0000 = hermite(old[3 + s0], old[3 + s1], old[3 + s2], old[3 + s3], tx); - const c1 = hermite(old[s4], old[s5], old[s6], old[s7], tx); - const c11 = hermite(old[1 + s4], old[1 + s5], old[1 + s6], old[1 + s7], tx); - const c111 = hermite(old[2 + s4], old[2 + s5], old[2 + s6], old[2 + s7], tx); - const c1111 = hermite(old[3 + s4], old[3 + s5], old[3 + s6], old[3 + s7], tx); - const c2 = hermite(old[s8], old[s9], old[s10], old[s11], tx); - const c22 = hermite(old[1 + s8], old[1 + s9], old[1 + s10], old[1 + s11], tx); - const c222 = hermite(old[2 + s8], old[2 + s9], old[2 + s10], old[2 + s11], tx); - const c2222 = hermite(old[3 + s8], old[3 + s9], old[3 + s10], old[3 + s11], tx); - const c3 = hermite(old[s12], old[s13], old[s14], old[s15], tx); - const c33 = hermite(old[1 + s12], old[1 + s13], old[1 + s14], old[1 + s15], tx); - const c333 = hermite(old[2 + s12], old[2 + s13], old[2 + s14], old[2 + s15], tx); - const c3333 = hermite(old[3 + s12], old[3 + s13], old[3 + s14], old[3 + s15], tx); - u82[offset++] = hermite(c0, c1, c2, c3, ty); - u82[offset++] = hermite(c00, c11, c22, c33, ty); - u82[offset++] = hermite(c000, c111, c222, c333, ty); - u82[offset++] = hermite(c0000, c1111, c2222, c3333, ty); - } - } - framebuffer2.width = width; - framebuffer2.height = height; - framebuffer2.u8 = new Uint8Array(u82.buffer); - framebuffer2.view = new DataView(u82.buffer); - framebuffer2.u32 = new Uint32Array(u82.buffer); -} - -// v2/ops/rotate.js -var rotate_exports = {}; -__export(rotate_exports, { - rotate: () => rotate, - rotate180: () => rotate180, - rotate270: () => rotate270, - rotate90: () => rotate90 -}); -function rotate180(framebuffer2) { - framebuffer2.u32.reverse(); -} -function rotate90(framebuffer2) { - const u323 = framebuffer2.u32; - const old = framebuffer2.u32.slice(); - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - framebuffer2.width = height; - framebuffer2.height = width; - for (let y = 0 | 0; y < width; y++) { - const yoffset = y * width; - const height1y = height - 1 - y; - for (let x2 = 0 | 0; x2 < height; x2++) { - u323[height1y + x2 * width] = old[x2 + yoffset]; - } - } -} -function rotate270(framebuffer2) { - const u323 = framebuffer2.u32; - const old = framebuffer2.u32.slice(); - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - framebuffer2.width = height; - framebuffer2.height = width; - for (let y = 0 | 0; y < width; y++) { - const yoffset = y * width | 0; - const soffset = y + width * (width - 1) | 0; - for (let x2 = 0 | 0; x2 < height; x2++) { - u323[soffset - x2 * width] = old[x2 + yoffset]; - } - } -} -function rotate(deg, framebuffer2, resize) { - const rad = Math.PI * ((360 - deg) / 180); - const sin = Math.sin(rad); - const cos = Math.cos(rad); - const width = (resize ? Math.abs(framebuffer2.width * sin) + Math.abs(framebuffer2.height * cos) : framebuffer2.width) | 0; - const height = (resize ? Math.abs(framebuffer2.width * cos) + Math.abs(framebuffer2.height * sin) : framebuffer2.height) | 0; - const same_size = width === framebuffer2.width && height === framebuffer2.height; - const inn = same_size ? framebuffer2.clone() : framebuffer2; - const out = { width, height, u8: same_size ? framebuffer2.u8 : new Uint8Array(4 * width * height) }; - const out_cx = width / 2 - 0.5; - const out_cy = height / 2 - 0.5; - const src_cx = framebuffer2.width / 2 - 0.5; - const src_cy = framebuffer2.height / 2 - 0.5; - let h = 0; - do { - let w = 0; - const ysin = src_cx - sin * (h - out_cy); - const ycos = src_cy + cos * (h - out_cy); - do { - interpolate(inn, out, w, h, ysin + cos * (w - out_cx), ycos + sin * (w - out_cx)); - } while (w++ < width); - } while (h++ < height); - framebuffer2.u8 = out.u8; - framebuffer2.width = width; - framebuffer2.height = height; - framebuffer2.view = new DataView(out.u8.buffer, out.u8.byteOffset, out.u8.byteLength); - framebuffer2.u32 = new Uint32Array(out.u8.buffer, out.u8.byteOffset, out.u8.byteLength / 4); -} -function interpolate(inn, out, x0, y0, x1, y1) { - const x2 = ~~x1; - const y2 = ~~y1; - const xq = x1 - x2; - const yq = y1 - y2; - const offset = 4 * (x0 + y0 * out.width); - const ref = { r: 0, g: 0, b: 0, a: 0 }; - pawn(x2, y2, (1 - xq) * (1 - yq), ref, inn); - pawn(1 + x2, y2, xq * (1 - yq), ref, inn); - pawn(x2, 1 + y2, (1 - xq) * yq, ref, inn); - pawn(1 + x2, 1 + y2, xq * yq, ref, inn); - out.u8[3 + offset] = ref.a; - out.u8[offset] = ref.r / ref.a; - out.u8[1 + offset] = ref.g / ref.a; - out.u8[2 + offset] = ref.b / ref.a; -} -function pawn(point0, point1, weight, ref, inn) { - if (point0 > 0 && point1 > 0 && point0 < inn.width && point1 < inn.height) { - const offset = 4 * (point0 + point1 * inn.width); - const wa = weight * inn.u8[3 + offset]; - ref.a += wa; - ref.r += wa * inn.u8[offset]; - ref.g += wa * inn.u8[1 + offset]; - ref.b += wa * inn.u8[2 + offset]; - } -} - -// v2/ops/overlay.js -var overlay_exports = {}; -__export(overlay_exports, { - blend: () => blend2, - replace: () => replace -}); -function replace(bg, fg, x2, y) { - const b32 = bg.u32; - const f32 = fg.u32; - const fw = fg.width | 0; - const bw = bg.width | 0; - const fh = fg.height | 0; - const bh = bg.height | 0; - const ox = (x2 > 0 ? 0 : -x2) | 0; - const oy = (y > 0 ? 0 : -y) | 0; - const top = (y > 0 ? y : 0) | 0; - const left = (x2 > 0 ? x2 : 0) | 0; - const width = Math.min(bw, x2 + fw) - left | 0; - const height = Math.min(bh, y + fh) - top | 0; - if (0 >= width || 0 >= height) - return; - for (let yy = 0 | 0; yy < height; yy++) { - const yyoffset = ox + fw * (yy + oy); - const yoffset = left + bw * (yy + top); - b32.subarray(yoffset, width + yoffset).set(f32.subarray(yyoffset, width + yyoffset)); - } -} -function blend2(bg, fg, x2, y) { - const b32 = bg.u32; - const f32 = fg.u32; - const fw = fg.width | 0; - const bw = bg.width | 0; - const fh = fg.height | 0; - const bh = bg.height | 0; - const ox = (x2 > 0 ? 0 : -x2) | 0; - const oy = (y > 0 ? 0 : -y) | 0; - const top = (y > 0 ? y : 0) | 0; - const left = (x2 > 0 ? x2 : 0) | 0; - const width = Math.min(bw, x2 + fw) - left | 0; - const height = Math.min(bh, y + fh) - top | 0; - if (0 >= width || 0 >= height) - return; - for (let yy = 0 | 0; yy < height; yy++) { - const yyoffset = ox + fw * (yy + oy); - const yoffset = left + bw * (yy + top); - for (let xx = 0 | 0; xx < width; xx++) { - const F = f32[xx + yyoffset]; - const fa = F >> 24 & 255; - if (fa === 0) - continue; - else if (fa === 255) - b32[xx + yoffset] = F; - else { - const alpha = 1 + fa; - const inv_alpha = 256 - fa; - const B = b32[xx + yoffset]; - const r = alpha * (F & 255) + inv_alpha * (B & 255) >> 8; - const g = alpha * (F >> 8 & 255) + inv_alpha * (B >> 8 & 255) >> 8; - const b = alpha * (F >> 16 & 255) + inv_alpha * (B >> 16 & 255) >> 8; - b32[xx + yoffset] = Math.max(fa, B >> 24 & 255) << 24 | (b & 255) << 16 | (g & 255) << 8 | r; - } - } - } -} - -// v2/ops/iterator.js -var iterator_exports = {}; -__export(iterator_exports, { - cords: () => cords, - rgba: () => rgba, - u32: () => u32 -}); -function* cords(framebuffer2) { - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - for (let y = 0 | 0; y < height; y++) { - for (let x2 = 0 | 0; x2 < width; x2++) - yield [x2, y]; - } -} -function* rgba(framebuffer2) { - let offset = 0 | 0; - const u82 = framebuffer2.u8; - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - for (let y = 0 | 0; y < height; y++) { - for (let x2 = 0 | 0; x2 < width; x2++) { - yield [x2, y, u82.subarray(offset, offset += 4)]; - } - } -} -function* u32(framebuffer2) { - let offset = 0 | 0; - const view3 = framebuffer2.view; - const width = framebuffer2.width | 0; - const height = framebuffer2.height | 0; - for (let y = 0 | 0; y < height; y++) { - for (let x2 = 0 | 0; x2 < width; x2++) { - yield [x2, y, view3.getUint32(offset, false)]; - offset += 4; - } - } -} - -// png/src/crc.js -var table = new Uint32Array([ - 0, - 1996959894, - 3993919788, - 2567524794, - 124634137, - 1886057615, - 3915621685, - 2657392035, - 249268274, - 2044508324, - 3772115230, - 2547177864, - 162941995, - 2125561021, - 3887607047, - 2428444049, - 498536548, - 1789927666, - 4089016648, - 2227061214, - 450548861, - 1843258603, - 4107580753, - 2211677639, - 325883990, - 1684777152, - 4251122042, - 2321926636, - 335633487, - 1661365465, - 4195302755, - 2366115317, - 997073096, - 1281953886, - 3579855332, - 2724688242, - 1006888145, - 1258607687, - 3524101629, - 2768942443, - 901097722, - 1119000684, - 3686517206, - 2898065728, - 853044451, - 1172266101, - 3705015759, - 2882616665, - 651767980, - 1373503546, - 3369554304, - 3218104598, - 565507253, - 1454621731, - 3485111705, - 3099436303, - 671266974, - 1594198024, - 3322730930, - 2970347812, - 795835527, - 1483230225, - 3244367275, - 3060149565, - 1994146192, - 31158534, - 2563907772, - 4023717930, - 1907459465, - 112637215, - 2680153253, - 3904427059, - 2013776290, - 251722036, - 2517215374, - 3775830040, - 2137656763, - 141376813, - 2439277719, - 3865271297, - 1802195444, - 476864866, - 2238001368, - 4066508878, - 1812370925, - 453092731, - 2181625025, - 4111451223, - 1706088902, - 314042704, - 2344532202, - 4240017532, - 1658658271, - 366619977, - 2362670323, - 4224994405, - 1303535960, - 984961486, - 2747007092, - 3569037538, - 1256170817, - 1037604311, - 2765210733, - 3554079995, - 1131014506, - 879679996, - 2909243462, - 3663771856, - 1141124467, - 855842277, - 2852801631, - 3708648649, - 1342533948, - 654459306, - 3188396048, - 3373015174, - 1466479909, - 544179635, - 3110523913, - 3462522015, - 1591671054, - 702138776, - 2966460450, - 3352799412, - 1504918807, - 783551873, - 3082640443, - 3233442989, - 3988292384, - 2596254646, - 62317068, - 1957810842, - 3939845945, - 2647816111, - 81470997, - 1943803523, - 3814918930, - 2489596804, - 225274430, - 2053790376, - 3826175755, - 2466906013, - 167816743, - 2097651377, - 4027552580, - 2265490386, - 503444072, - 1762050814, - 4150417245, - 2154129355, - 426522225, - 1852507879, - 4275313526, - 2312317920, - 282753626, - 1742555852, - 4189708143, - 2394877945, - 397917763, - 1622183637, - 3604390888, - 2714866558, - 953729732, - 1340076626, - 3518719985, - 2797360999, - 1068828381, - 1219638859, - 3624741850, - 2936675148, - 906185462, - 1090812512, - 3747672003, - 2825379669, - 829329135, - 1181335161, - 3412177804, - 3160834842, - 628085408, - 1382605366, - 3423369109, - 3138078467, - 570562233, - 1426400815, - 3317316542, - 2998733608, - 733239954, - 1555261956, - 3268935591, - 3050360625, - 752459403, - 1541320221, - 2607071920, - 3965973030, - 1969922972, - 40735498, - 2617837225, - 3943577151, - 1913087877, - 83908371, - 2512341634, - 3803740692, - 2075208622, - 213261112, - 2463272603, - 3855990285, - 2094854071, - 198958881, - 2262029012, - 4057260610, - 1759359992, - 534414190, - 2176718541, - 4139329115, - 1873836001, - 414664567, - 2282248934, - 4279200368, - 1711684554, - 285281116, - 2405801727, - 4167216745, - 1634467795, - 376229701, - 2685067896, - 3608007406, - 1308918612, - 956543938, - 2808555105, - 3495958263, - 1231636301, - 1047427035, - 2932959818, - 3654703836, - 1088359270, - 936918e3, - 2847714899, - 3736837829, - 1202900863, - 817233897, - 3183342108, - 3401237130, - 1404277552, - 615818150, - 3134207493, - 3453421203, - 1423857449, - 601450431, - 3009837614, - 3294710456, - 1567103746, - 711928724, - 3020668471, - 3272380065, - 1510334235, - 755167117 -]); -function crc32(buffer) { - let offset = 0 | 0; - let crc = 4294967295 | 0; - const bl = buffer.length - 4 | 0; - while (bl > offset) { - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - } - while (offset < buffer.length) { - crc = table[(crc ^ buffer[offset++]) & 255] ^ crc >>> 8; - } - return (crc ^ 4294967295) >>> 0; -} - -// png/src/mem.js -function view2(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) - return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) - return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} -function from_parts(buffers, shared = false) { - let length = 0; - let offset = 0; - buffers.forEach((buffer) => length += buffer.byteLength == null ? buffer.length : buffer.byteLength); - const u82 = new Uint8Array(shared ? new SharedArrayBuffer(length) : length); - buffers.forEach((buffer) => { - const ref = Array.isArray(buffer) ? buffer : view2(buffer, true); - u82.set(ref, offset); - offset += ref.length; - }); - return u82; -} - -// png/src/zlib.js -var u8 = Uint8Array; -var u16 = Uint16Array; -var u322 = Uint32Array; -var clim = u8.of(16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15); -var fleb = u8.of(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0); -var fdeb = u8.of(0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0); -var freb = function(eb, start) { - var b = new u16(31); - for (var i = 0; i < 31; ++i) { - b[i] = start += 1 << eb[i - 1]; - } - var r = new u322(b[30]); - for (var i = 1; i < 30; ++i) { - for (var j = b[i]; j < b[i + 1]; ++j) { - r[j] = j - b[i] << 5 | i; - } - } - return [b, r]; -}; -var _a = freb(fleb, 2); -var fl = _a[0]; -var revfl = _a[1]; -fl[28] = 258, revfl[258] = 28; -var _b = freb(fdeb, 0); -var fd = _b[0]; -var revfd = _b[1]; -var rev = new u16(32768); -for (i = 0; i < 32768; ++i) { - x = (i & 43690) >>> 1 | (i & 21845) << 1; - x = (x & 52428) >>> 2 | (x & 13107) << 2; - x = (x & 61680) >>> 4 | (x & 3855) << 4; - rev[i] = ((x & 65280) >>> 8 | (x & 255) << 8) >>> 1; -} -var i; -var x; -var hMap = function(cd, mb, r) { - var s = cd.length; - var i = 0; - var l = new u16(mb); - for (; i < s; ++i) - ++l[cd[i] - 1]; - var le = new u16(mb); - for (i = 0; i < mb; ++i) { - le[i] = le[i - 1] + l[i - 1] << 1; - } - var co; - if (r) { - co = new u16(1 << mb); - var rvb = 15 - mb; - for (i = 0; i < s; ++i) { - if (cd[i]) { - var sv = i << 4 | cd[i]; - var r_1 = mb - cd[i]; - var v = le[cd[i] - 1]++ << r_1; - for (var m = v | (1 << r_1) - 1; v <= m; ++v) { - co[rev[v] >>> rvb] = sv; - } - } - } - } else { - co = new u16(s); - for (i = 0; i < s; ++i) { - if (cd[i]) { - co[i] = rev[le[cd[i] - 1]++] >>> 15 - cd[i]; - } - } - } - return co; -}; -var flt = new u8(288); -for (i = 0; i < 144; ++i) - flt[i] = 8; -var i; -for (i = 144; i < 256; ++i) - flt[i] = 9; -var i; -for (i = 256; i < 280; ++i) - flt[i] = 7; -var i; -for (i = 280; i < 288; ++i) - flt[i] = 8; -var i; -var fdt = new u8(32); -for (i = 0; i < 32; ++i) - fdt[i] = 5; -var i; -var flm = hMap(flt, 9, 0); -var flrm = hMap(flt, 9, 1); -var fdm = hMap(fdt, 5, 0); -var fdrm = hMap(fdt, 5, 1); -var max = function(a) { - var m = a[0]; - for (var i = 1; i < a.length; ++i) { - if (a[i] > m) - m = a[i]; - } - return m; -}; -var bits = function(d, p, m) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8) >> (p & 7) & m; -}; -var bits16 = function(d, p) { - var o = p >> 3 | 0; - return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7); -}; -var shft = function(p) { - return (p >> 3 | 0) + (p & 7 && 1); -}; -var slc = function(v, s, e) { - if (s == null || s < 0) - s = 0; - if (e == null || e > v.length) - e = v.length; - var n = new (v instanceof u16 ? u16 : v instanceof u322 ? u322 : u8)(e - s); - n.set(v.subarray(s, e)); - return n; -}; -var inflt = function(dat, buf, st) { - var sl = dat.length; - if (!sl || st && !st.l && sl < 5) - return buf || new u8(0); - var noBuf = !buf || st; - var noSt = !st || st.i; - if (!st) - st = {}; - if (!buf) - buf = new u8(sl * 3); - var cbuf = function(l2) { - var bl = buf.length; - if (l2 > bl) { - var nbuf = new u8(Math.max(bl * 2, l2)); - nbuf.set(buf); - buf = nbuf; - } - }; - var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; - var tbts = sl * 8; - do { - if (!lm) { - st.f = final = bits(dat, pos, 1); - var type = bits(dat, pos + 1, 3); - pos += 3; - if (!type) { - var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l; - if (t > sl) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + l); - buf.set(dat.subarray(s, t), bt); - st.b = bt += l, st.p = pos = t * 8; - continue; - } else if (type === 1) - lm = flrm, dm = fdrm, lbt = 9, dbt = 5; - else if (type === 2) { - var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; - var tl = hLit + bits(dat, pos + 5, 31) + 1; - pos += 14; - var ldt = new u8(tl); - var clt = new u8(19); - for (var i = 0; i < hcLen; ++i) { - clt[clim[i]] = bits(dat, pos + i * 3, 7); - } - pos += hcLen * 3; - var clb = max(clt), clbmsk = (1 << clb) - 1; - var clm = hMap(clt, clb, 1); - for (var i = 0; i < tl; ) { - var r = clm[bits(dat, pos, clbmsk)]; - pos += r & 15; - var s = r >>> 4; - if (s < 16) { - ldt[i++] = s; - } else { - var c = 0, n = 0; - if (s === 16) - n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; - else if (s === 17) - n = 3 + bits(dat, pos, 7), pos += 3; - else if (s === 18) - n = 11 + bits(dat, pos, 127), pos += 7; - while (n--) - ldt[i++] = c; - } - } - var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); - lbt = max(lt); - dbt = max(dt); - lm = hMap(lt, lbt, 1); - dm = hMap(dt, dbt, 1); - } else - throw "invalid block type"; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - } - if (noBuf) - cbuf(bt + 131072); - var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; - var lpos = pos; - for (; ; lpos = pos) { - var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; - pos += c & 15; - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (!c) - throw "invalid length/literal"; - if (sym < 256) - buf[bt++] = sym; - else if (sym === 256) { - lpos = pos, lm = null; - break; - } else { - var add = sym - 254; - if (sym > 264) { - var i = sym - 257, b = fleb[i]; - add = bits(dat, pos, (1 << b) - 1) + fl[i]; - pos += b; - } - var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; - if (!d) - throw "invalid distance"; - pos += d & 15; - var dt = fd[dsym]; - if (dsym > 3) { - var b = fdeb[dsym]; - dt += bits16(dat, pos) & (1 << b) - 1, pos += b; - } - if (pos > tbts) { - if (noSt) - throw "unexpected EOF"; - break; - } - if (noBuf) - cbuf(bt + 131072); - var end = bt + add; - for (; bt < end; bt += 4) { - buf[bt] = buf[bt - dt]; - buf[bt + 1] = buf[bt + 1 - dt]; - buf[bt + 2] = buf[bt + 2 - dt]; - buf[bt + 3] = buf[bt + 3 - dt]; - } - bt = end; - } - } - st.l = lm, st.p = lpos, st.b = bt; - if (lm) - final = 1, st.m = lbt, st.d = dm, st.n = dbt; - } while (!final); - return bt === buf.length ? buf : slc(buf, 0, bt); -}; -var wbits = function(d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; -}; -var wbits16 = function(d, p, v) { - v <<= p & 7; - var o = p >> 3 | 0; - d[o] |= v; - d[o + 1] |= v >>> 8; - d[o + 2] |= v >>> 16; -}; -var hTree = function(d, mb) { - var t = []; - for (var i = 0; i < d.length; ++i) { - if (d[i]) - t.push({ s: i, f: d[i] }); - } - var s = t.length; - var t2 = t.slice(); - if (!s) - return [et, 0]; - if (s === 1) { - var v = new u8(t[0].s + 1); - v[t[0].s] = 1; - return [v, 1]; - } - t.sort(function(a, b) { - return a.f - b.f; - }); - t.push({ s: -1, f: 25001 }); - var l = t[0], r = t[1], i0 = 0, i1 = 1, i2 = 2; - t[0] = { s: -1, f: l.f + r.f, l, r }; - while (i1 !== s - 1) { - l = t[t[i0].f < t[i2].f ? i0++ : i2++]; - r = t[i0 !== i1 && t[i0].f < t[i2].f ? i0++ : i2++]; - t[i1++] = { s: -1, f: l.f + r.f, l, r }; - } - var maxSym = t2[0].s; - for (var i = 1; i < s; ++i) { - if (t2[i].s > maxSym) - maxSym = t2[i].s; - } - var tr = new u16(maxSym + 1); - var mbt = ln(t[i1 - 1], tr, 0); - if (mbt > mb) { - var i = 0, dt = 0; - var lft = mbt - mb, cst = 1 << lft; - t2.sort(function(a, b) { - return tr[b.s] - tr[a.s] || a.f - b.f; - }); - for (; i < s; ++i) { - var i2_1 = t2[i].s; - if (tr[i2_1] > mb) { - dt += cst - (1 << mbt - tr[i2_1]); - tr[i2_1] = mb; - } else - break; - } - dt >>>= lft; - while (dt > 0) { - var i2_2 = t2[i].s; - if (tr[i2_2] < mb) - dt -= 1 << mb - tr[i2_2]++ - 1; - else - ++i; - } - for (; i >= 0 && dt; --i) { - var i2_3 = t2[i].s; - if (tr[i2_3] === mb) { - --tr[i2_3]; - ++dt; - } - } - mbt = mb; - } - return [new u8(tr), mbt]; -}; -var ln = function(n, l, d) { - return n.s === -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d; -}; -var lc = function(c) { - var s = c.length; - while (s && !c[--s]) - ; - var cl = new u16(++s); - var cli = 0, cln = c[0], cls = 1; - var w = function(v) { - cl[cli++] = v; - }; - for (var i = 1; i <= s; ++i) { - if (c[i] === cln && i !== s) - ++cls; - else { - if (!cln && cls > 2) { - for (; cls > 138; cls -= 138) - w(32754); - if (cls > 2) { - w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); - cls = 0; - } - } else if (cls > 3) { - w(cln), --cls; - for (; cls > 6; cls -= 6) - w(8304); - if (cls > 2) - w(cls - 3 << 5 | 8208), cls = 0; - } - while (cls--) - w(cln); - cls = 1; - cln = c[i]; - } - } - return [cl.subarray(0, cli), s]; -}; -var clen = function(cf, cl) { - var l = 0; - for (var i = 0; i < cl.length; ++i) - l += cf[i] * cl[i]; - return l; -}; -var wfblk = function(out, pos, dat) { - var s = dat.length; - var o = shft(pos + 2); - out[o] = s & 255; - out[o + 1] = s >>> 8; - out[o + 2] = out[o] ^ 255; - out[o + 3] = out[o + 1] ^ 255; - for (var i = 0; i < s; ++i) - out[o + i + 4] = dat[i]; - return (o + 4 + s) * 8; -}; -var wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) { - wbits(out, p++, final); - ++lf[256]; - var _a2 = hTree(lf, 15), dlt = _a2[0], mlb = _a2[1]; - var _b2 = hTree(df, 15), ddt = _b2[0], mdb = _b2[1]; - var _c = lc(dlt), lclt = _c[0], nlc = _c[1]; - var _d = lc(ddt), lcdt = _d[0], ndc = _d[1]; - var lcfreq = new u16(19); - for (var i = 0; i < lclt.length; ++i) - lcfreq[lclt[i] & 31]++; - for (var i = 0; i < lcdt.length; ++i) - lcfreq[lcdt[i] & 31]++; - var _e = hTree(lcfreq, 7), lct = _e[0], mlcb = _e[1]; - var nlcc = 19; - for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) - ; - var flen = bl + 5 << 3; - var ftlen = clen(lf, flt) + clen(df, fdt) + eb; - var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + (2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]); - if (flen <= ftlen && flen <= dtlen) - return wfblk(out, p, dat.subarray(bs, bs + bl)); - var lm, ll, dm, dl; - wbits(out, p, 1 + (dtlen < ftlen)), p += 2; - if (dtlen < ftlen) { - lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; - var llm = hMap(lct, mlcb, 0); - wbits(out, p, nlc - 257); - wbits(out, p + 5, ndc - 1); - wbits(out, p + 10, nlcc - 4); - p += 14; - for (var i = 0; i < nlcc; ++i) - wbits(out, p + 3 * i, lct[clim[i]]); - p += 3 * nlcc; - var lcts = [lclt, lcdt]; - for (var it = 0; it < 2; ++it) { - var clct = lcts[it]; - for (var i = 0; i < clct.length; ++i) { - var len = clct[i] & 31; - wbits(out, p, llm[len]), p += lct[len]; - if (len > 15) - wbits(out, p, clct[i] >>> 5 & 127), p += clct[i] >>> 12; - } - } - } else { - lm = flm, ll = flt, dm = fdm, dl = fdt; - } - for (var i = 0; i < li; ++i) { - if (syms[i] > 255) { - var len = syms[i] >>> 18 & 31; - wbits16(out, p, lm[len + 257]), p += ll[len + 257]; - if (len > 7) - wbits(out, p, syms[i] >>> 23 & 31), p += fleb[len]; - var dst = syms[i] & 31; - wbits16(out, p, dm[dst]), p += dl[dst]; - if (dst > 3) - wbits16(out, p, syms[i] >>> 5 & 8191), p += fdeb[dst]; - } else { - wbits16(out, p, lm[syms[i]]), p += ll[syms[i]]; - } - } - wbits16(out, p, lm[256]); - return p + ll[256]; -}; -var deo = u322.of(65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632); -var et = new u8(0); -var dflt = function(dat, lvl, plvl, pre, post, lst) { - var s = dat.length; - var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7e3)) + post); - var w = o.subarray(pre, o.length - post); - var pos = 0; - if (!lvl || s < 8) { - for (var i = 0; i <= s; i += 65535) { - var e = i + 65535; - if (e < s) { - pos = wfblk(w, pos, dat.subarray(i, e)); - } else { - w[i] = lst; - pos = wfblk(w, pos, dat.subarray(i, s)); - } - } - } else { - var opt = deo[lvl - 1]; - var n = opt >>> 13, c = opt & 8191; - var msk_1 = (1 << plvl) - 1; - var prev = new u16(32768), head = new u16(msk_1 + 1); - var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; - var hsh = function(i2) { - return (dat[i2] ^ dat[i2 + 1] << bs1_1 ^ dat[i2 + 2] << bs2_1) & msk_1; - }; - var syms = new u322(25e3); - var lf = new u16(288), df = new u16(32); - var lc_1 = 0, eb = 0, i = 0, li = 0, wi = 0, bs = 0; - for (; i < s; ++i) { - var hv = hsh(i); - var imod = i & 32767, pimod = head[hv]; - prev[imod] = pimod; - head[hv] = imod; - if (wi <= i) { - var rem = s - i; - if ((lc_1 > 7e3 || li > 24576) && rem > 423) { - pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i - bs, pos); - li = lc_1 = eb = 0, bs = i; - for (var j = 0; j < 286; ++j) - lf[j] = 0; - for (var j = 0; j < 30; ++j) - df[j] = 0; - } - var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; - if (rem > 2 && hv === hsh(i - dif)) { - var maxn = Math.min(n, rem) - 1; - var maxd = Math.min(32767, i); - var ml = Math.min(258, rem); - while (dif <= maxd && --ch_1 && imod !== pimod) { - if (dat[i + l] === dat[i + l - dif]) { - var nl = 0; - for (; nl < ml && dat[i + nl] === dat[i + nl - dif]; ++nl) - ; - if (nl > l) { - l = nl, d = dif; - if (nl > maxn) - break; - var mmd = Math.min(dif, nl - 2); - var md = 0; - for (var j = 0; j < mmd; ++j) { - var ti = i - dif + j + 32768 & 32767; - var pti = prev[ti]; - var cd = ti - pti + 32768 & 32767; - if (cd > md) - md = cd, pimod = ti; - } - } - } - imod = pimod, pimod = prev[imod]; - dif += imod - pimod + 32768 & 32767; - } - } - if (d) { - syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; - var lin = revfl[l] & 31, din = revfd[d] & 31; - eb += fleb[lin] + fdeb[din]; - ++lf[257 + lin]; - ++df[din]; - wi = i + l; - ++lc_1; - } else { - syms[li++] = dat[i]; - ++lf[dat[i]]; - } - } - } - pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i - bs, pos); - if (!lst && pos & 7) - pos = wfblk(w, pos + 1, et); - } - return slc(o, 0, pre + shft(pos) + post); -}; -var adler = function() { - var a = 1, b = 0; - return { - p: function(d) { - var n = a, m = b; - var l = d.length | 0; - for (var i = 0; i !== l; ) { - var e = Math.min(i + 2655, l); - for (; i < e; ++i) - m += n += d[i]; - n = (n & 65535) + 15 * (n >> 16), m = (m & 65535) + 15 * (m >> 16); - } - a = n, b = m; - }, - d: function() { - a %= 65521, b %= 65521; - return (a & 255) << 24 | a >>> 8 << 16 | (b & 255) << 8 | b >>> 8; - } - }; -}; -var dopt = function(dat, opt, pre, post, st) { - return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 12 + opt.mem, pre, post, !st); -}; -var wbytes = function(d, b, v) { - for (; v; ++b) - d[b] = v, v >>>= 8; -}; -var zlh = function(c, o) { - var lv = o.level, fl2 = lv === 0 ? 0 : lv < 6 ? 1 : lv === 9 ? 3 : 2; - c[0] = 120, c[1] = fl2 << 6 | (fl2 ? 32 - 2 * fl2 : 1); -}; -var zlv = function(d) { - if ((d[0] & 15) !== 8 || d[0] >>> 4 > 7 || (d[0] << 8 | d[1]) % 31) - throw "invalid zlib data"; - if (d[1] & 32) - throw "invalid zlib data: preset dictionaries not supported"; -}; -function zlibSync(data, opts) { - if (!opts) - opts = {}; - var a = adler(); - a.p(data); - var d = dopt(data, opts, 2, 4); - return zlh(d, opts), wbytes(d, d.length - 4, a.d()), d; -} -function unzlibSync(data, out) { - return inflt((zlv(data), data.subarray(2, -4)), out); -} -function compress(buf, level) { - return zlibSync(buf, { level }); -} -function decompress(buf, limit) { - try { - return unzlibSync(buf, new Uint8Array(limit)); - } catch (err) { - throw err.message ? err : new Error(err); - } -} - -// png/src/png.js -var __IHDR__ = new Uint8Array([73, 72, 68, 82]); -var __IDAT__ = new Uint8Array([73, 68, 65, 84]); -var __IEND__ = new Uint8Array([73, 69, 78, 68]); -var __IEND_CRC__ = crc32(new Uint8Array([73, 69, 78, 68])); -var HEAD = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); -var color_types = { - GREYSCALE: 0, - TRUECOLOR: 2, - INDEXED_COLOR: 3, - GREYSCALE_ALPHA: 4, - TRUECOLOR_ALPHA: 6 -}; -var channels_to_color_type = { - 1: color_types.GREYSCALE, - 2: color_types.GREYSCALE_ALPHA, - 3: color_types.TRUECOLOR, - 4: color_types.TRUECOLOR_ALPHA -}; -var utf8encoder = new TextEncoder(); -function encode(data, { text, width, height, channels, depth = 8, level = 0 }) { - let offset = 0; - let tmp_offset = 0; - const row_length = width * channels; - const tmp = new Uint8Array(height + data.length); - while (offset < data.length) { - tmp[tmp_offset++] = 0; - tmp.set(data.subarray(offset, offset += row_length), tmp_offset); - tmp_offset += row_length; - } - if (text) { - let chunks = []; - for (const key in text) { - if (!text[key]) - continue; - const kb = utf8encoder.encode(key); - const tb = utf8encoder.encode(text[key]); - const chunk = new Uint8Array(1 + 12 + kb.length + tb.length); - const view4 = new DataView(chunk.buffer); - chunk[4] = 116; - chunk[5] = 69; - chunk[6] = 88; - chunk[7] = 116; - chunk.set(kb, 8); - chunks.push(chunk); - chunk.set(tb, 9 + kb.length); - view4.setUint32(0, chunk.length - 12); - view4.setUint32(chunk.length - 4, crc32(chunk.subarray(4, chunk.length - 4))); - } - text = from_parts(chunks); - } - offset = text ? text.length : 0; - const compressed = compress(tmp, level); - const array = new Uint8Array(49 + offset + HEAD.length + compressed.length); - array[26] = 0; - array[27] = 0; - array[28] = 0; - array[24] = depth; - array.set(HEAD, 0); - array.set(__IHDR__, 12); - array.set(__IDAT__, 37); - array.set(compressed, 41); - array[25] = channels_to_color_type[channels]; - if (text) - array.set(text, 45 + compressed.length); - array.set(__IEND__, 49 + offset + compressed.length); - const view3 = new DataView(array.buffer); - view3.setUint32(8, 13); - view3.setUint32(16, width); - view3.setUint32(20, height); - view3.setUint32(33, compressed.length); - view3.setUint32(45 + offset + compressed.length, 0); - view3.setUint32(53 + offset + compressed.length, __IEND_CRC__); - view3.setUint32(29, crc32(new Uint8Array(array.buffer, 12, 17))); - view3.setUint32(41 + compressed.length, crc32(new Uint8Array(array.buffer, 37, 4 + compressed.length))); - return array; -} -function decode(array) { - let view3 = new DataView(array.buffer, array.byteOffset, array.byteLength); - const width = view3.getUint32(16); - const height = view3.getUint32(20); - const bpc = array[24]; - const pixel_type = array[25]; - let channels = { 3: 1, 0: 1, 4: 2, 2: 3, 6: 4 }[pixel_type]; - const bytespp = channels * bpc / 8; - const row_length = width * bytespp; - let pixels = new Uint8Array(height * row_length); - let offset = 0; - let p_offset = 0; - let c_offset = 33; - const chunks = []; - let palette, alphaPalette; - const maxSearchOffset = array.length - 5; - let type; - while ((type = view3.getUint32(4 + c_offset)) !== 1229278788) { - if (type === 1229209940) - chunks.push(array.subarray(8 + c_offset, 8 + c_offset + view3.getUint32(c_offset))); - else if (type === 1347179589) { - if (palette) - throw new Error("PLTE can only occur once in an image"); - palette = new Uint32Array(view3.getUint32(c_offset)); - for (let pxlOffset = 0; pxlOffset < palette.length * 8; pxlOffset += 3) - palette[pxlOffset / 3] = array[8 + c_offset + pxlOffset] << 24 | array[8 + c_offset + pxlOffset + 1] << 16 | array[8 + c_offset + pxlOffset + 2] << 8 | 255; - } else if (type === 1951551059) { - if (alphaPalette) - throw new Error("tRNS can only occur once in an image"); - alphaPalette = new Uint8Array(view3.getUint32(c_offset)); - for (let i = 0; i < alphaPalette.length; i++) - alphaPalette[i] = array[8 + c_offset + i]; - } - c_offset += 4 + 4 + 4 + view3.getUint32(c_offset); - if (c_offset > maxSearchOffset) - break; - } - array = decompress(chunks.length === 1 ? chunks[0] : from_parts(chunks), height + height * row_length); - while (offset < array.byteLength) { - const filter = array[offset++]; - const slice = array.subarray(offset, offset += row_length); - if (filter === 0) - pixels.set(slice, p_offset); - else if (filter === 1) - filter_1(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 2) - filter_2(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 3) - filter_3(slice, pixels, p_offset, bytespp, row_length); - else if (filter === 4) - filter_4(slice, pixels, p_offset, bytespp, row_length); - p_offset += row_length; - } - if (pixel_type === 3) { - if (!palette) - throw new Error("Indexed color PNG has no PLTE"); - if (alphaPalette) - for (let i = 0; i < alphaPalette.length; i++) - palette[i] &= 4294967040 | alphaPalette[i]; - channels = 4; - const newPixels = new Uint8Array(width * height * 4); - const pixelView = new DataView(newPixels.buffer, newPixels.byteOffset, newPixels.byteLength); - for (let i = 0; i < pixels.length; i++) - pixelView.setUint32(i * 4, palette[pixels[i]], false); - pixels = newPixels; - } - if (bpc !== 8) { - const newPixels = new Uint8Array(pixels.length / bpc * 8); - for (let i = 0; i < pixels.length; i += 2) - newPixels[i / 2] = pixels[i]; - pixels = newPixels; - } - if (channels !== 4) { - const newPixels = new Uint8Array(width * height * 4); - const view4 = new DataView(newPixels.buffer); - if (channels === 1) { - for (let i = 0; i < width * height; i++) { - const pixel = pixels[i]; - view4.setUint32(i * 4, pixel << 24 | pixel << 16 | pixel << 8 | 255, false); - } - } else if (channels === 2) { - for (let i = 0; i < width * height * 2; i += 2) { - const pixel = pixels[i]; - view4.setUint32(i * 2, pixel << 24 | pixel << 16 | pixel << 8 | pixels[i + 1], false); - } - } else if (channels === 3) { - newPixels.fill(255); - for (let i = 0; i < width * height; i++) - newPixels.set(pixels.subarray(i * 3, i * 3 + 3), i * 4); - } - pixels = newPixels; - } - return { width, height, buffer: pixels }; -} -function filter_1(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - while (i < bytespp) - pixels[i + p_offset] = slice[i++]; - while (i < row_length) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - bytespp]; -} -function filter_2(slice, pixels, p_offset, bytespp, row_length) { - if (p_offset === 0) - pixels.set(slice, p_offset); - else { - let i = 0; - while (i < row_length) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - } -} -function filter_3(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - if (p_offset === 0) { - while (i < bytespp) - pixels[i] = slice[i++]; - while (i < row_length) - pixels[i] = slice[i] + (pixels[i++ - bytespp] >> 1); - } else { - while (i < bytespp) - pixels[i + p_offset] = slice[i] + (pixels[i++ + p_offset - row_length] >> 1); - while (i < row_length) - pixels[i + p_offset] = slice[i] + (pixels[i + p_offset - bytespp] + pixels[i++ + p_offset - row_length] >> 1); - } -} -function filter_4(slice, pixels, p_offset, bytespp, row_length) { - let i = 0; - if (p_offset === 0) { - while (i < bytespp) - pixels[i] = slice[i++]; - while (i < row_length) - pixels[i] = slice[i] + pixels[i++ - bytespp]; - } else { - while (i < bytespp) - pixels[i + p_offset] = slice[i] + pixels[i++ + p_offset - row_length]; - while (i < row_length) { - const a = pixels[i + p_offset - bytespp]; - const b = pixels[i + p_offset - row_length]; - const c = pixels[i + p_offset - bytespp - row_length]; - const p = a + b - c; - const pa = Math.abs(p - a); - const pb = Math.abs(p - b); - const pc = Math.abs(p - c); - pixels[i + p_offset] = slice[i++] + (pa <= pb && pa <= pc ? a : pb <= pc ? b : c); - } - } -} - -// v2/framebuffer.mjs -var framebuffer = class { - constructor(width, height, buffer) { - this.width = width | 0; - this.height = height | 0; - this.u8 = buffer ? view(buffer) : new Uint8Array(4 * this.width * this.height); - this.view = new DataView(this.u8.buffer, this.u8.byteOffset, this.u8.byteLength); - this.u32 = new Uint32Array(this.u8.buffer, this.u8.byteOffset, this.u8.byteLength / 4); - if (this.u8.length !== 4 * this.width * this.height) - throw new RangeError("invalid capacity of buffer"); - } - [Symbol.iterator]() { - return iterator_exports.cords(this); - } - toString() { - return `framebuffer<${this.width}x${this.height}>`; - } - get(x2, y) { - return this.view.getUint32((x2 | 0) + (y | 0) * this.width, false); - } - clone() { - return new this.constructor(this.width, this.height, this.u8.slice()); - } - set(x2, y, color3) { - this.view.setUint32((x2 | 0) + (y | 0) * this.width, color3, false); - } - toJSON() { - return { width: this.width, height: this.height, buffer: Array.from(this.u8) }; - } - scale(type, factor) { - return this.resize(type, factor * this.width, factor * this.height); - } - overlay(frame, x2 = 0, y = 0) { - return overlay_exports.blend(this, frame, x2 | 0, y | 0), this; - } - replace(frame, x2 = 0, y = 0) { - return overlay_exports.replace(this, frame, x2 | 0, y | 0), this; - } - at(x2, y) { - const offset = 4 * ((x2 | 0) + (y | 0) * this.width); - return this.u8.subarray(offset, 4 + offset); - } - static from(framebuffer2) { - return new this(framebuffer2.width, framebuffer2.height, framebuffer2.u8 || framebuffer2.buffer); - } - static decode(format, buffer) { - if (format !== "png") - throw new RangeError("invalid image format"); - else - return framebuffer.from(decode(buffer)); - } - encode(format, options = {}) { - var _a2; - if (format !== "png") - throw new RangeError("invalid image format"); - else - return encode(this.u8, { channels: 4, width: this.width, height: this.height, level: (_a2 = { none: 0, fast: 3, default: 6, best: 9 }[options.compression]) != null ? _a2 : 3 }); - } - pixels(type) { - if (type === "rgba") - return iterator_exports.rgba(this); - if (!type || type === "int") - return iterator_exports.u32(this); - throw new RangeError("invalid iterator type"); - } - flip(type) { - if (type === "vertical") - flip_exports.vertical(this); - else if (type === "horizontal") - flip_exports.horizontal(this); - else - throw new RangeError("invalid flip type"); - return this; - } - crop(type, arg0, arg1, arg2, arg3) { - if (type === "circle") - crop_exports.circle(arg0 || 0, this); - else if (type === "box") - crop_exports.crop(arg0 | 0, arg1 | 0, arg2 | 0, arg3 | 0, this); - else - throw new RangeError("invalid crop type"); - return this; - } - cut(type, arg0, arg1, arg2, arg3) { - if (type === "circle") - return crop_exports.circle(arg0 || 0, this.clone()); - else if (type === "box") - return crop_exports.cut(arg0 | 0, arg1 | 0, arg2 | 0, arg3 | 0, this); - else - throw new RangeError("invalid cut type"); - } - rotate(deg, resize = true) { - if ((deg %= 360) === 0) - return this; - else if (deg === 90) - rotate_exports.rotate90(this); - else if (deg === 180) - rotate_exports.rotate180(this); - else if (deg === 270) - rotate_exports.rotate270(this); - else - rotate_exports.rotate(deg, this, resize); - return this; - } - blur(type, arg0) { - if (type === "cubic") - blur_exports.cubic(this); - else if (type === "box") - blur_exports.box(+arg0, this); - else if (type === "gaussian") - blur_exports.gaussian(+arg0, this); - else - throw new RangeError("invalid blur type"); - return this; - } - fill(color3) { - const type = typeof color3; - if (type === "function") - fill_exports.fn(color3, this); - else if (type === "number") - fill_exports.color(color3, this); - else if (color3 instanceof color) - fill_exports.color(color3.valueOf(), this); - else if (Array.isArray(color3)) - fill_exports.color(color_exports.from_rgba(...color3), this); - else - throw new TypeError("invalid fill color"); - return this; - } - swap(old, color3) { - const ot = typeof old; - const nt = typeof color3; - if (ot === nt && ot === "number") - fill_exports.swap(old, color3, this); - else if (old instanceof color && color3 instanceof color) - fill_exports.swap(old.valueOf(), color3.valueOf(), this); - else if (Array.isArray(old) && Array.isArray(color3)) - fill_exports.swap(color_exports.from_rgba(...old), color_exports.from_rgba(...color3), this); - else - throw new RangeError("invalid swap color"); - return this; - } - resize(type, width, height) { - if (width === this.width && height === this.height) - return this; - else if (type === "cubic") - resize_exports.cubic(width, height, this); - else if (type === "linear") - resize_exports.linear(width, height, this); - else if (type === "nearest") - resize_exports.nearest(width, height, this); - else - throw new RangeError("invalid resize type"); - return this; - } -}; -// Annotate the CommonJS export names for ESM import in node: -0 && (module.exports = { - Color -}); diff --git a/v2/framebuffer.mjs b/v2/framebuffer.mjs deleted file mode 100644 index e59efd4..0000000 --- a/v2/framebuffer.mjs +++ /dev/null @@ -1,126 +0,0 @@ -import Color from './ops/color.mjs'; -import { view } from './util/mem.mjs'; -import * as ops from './ops/index.mjs'; -import * as png from '../png/src/png.mjs'; - -export { Color }; - -// todo: tree shakable context -// todo: make errors more verbose - -export default class framebuffer { - constructor(width, height, buffer) { - this.width = width | 0; - this.height = height | 0; - this.u8 = buffer ? view(buffer) : new Uint8Array(4 * this.width * this.height); - this.view = new DataView(this.u8.buffer, this.u8.byteOffset, this.u8.byteLength); - this.u32 = new Uint32Array(this.u8.buffer, this.u8.byteOffset, this.u8.byteLength / 4); - if (this.u8.length !== 4 * this.width * this.height) throw new RangeError('invalid capacity of buffer'); - } - - [Symbol.iterator]() { return ops.iterator.cords(this); } - toString() { return `framebuffer<${this.width}x${this.height}>`; } - get(x, y) { return this.view.getUint32((x | 0) + (y | 0) * this.width, false); } - clone() { return new this.constructor(this.width, this.height, this.u8.slice()); } - set(x, y, color) { this.view.setUint32((x | 0) + (y | 0) * this.width, color, false); } - toJSON() { return { width: this.width, height: this.height, buffer: Array.from(this.u8) } } - scale(type, factor) { return this.resize(type, factor * this.width, factor * this.height); } - overlay(frame, x = 0, y = 0) { return (ops.overlay.blend(this, frame, x | 0, y | 0), this); } - replace(frame, x = 0, y = 0) { return (ops.overlay.replace(this, frame, x | 0, y | 0), this); } - at(x, y) { const offset = 4 * ((x | 0) + (y | 0) * this.width); return this.u8.subarray(offset, 4 + offset); } - static from(framebuffer) { return new this(framebuffer.width, framebuffer.height, framebuffer.u8 || framebuffer.buffer); } - static decode(format, buffer) { if (format !== 'png') throw new RangeError('invalid image format'); else return framebuffer.from(png.decode(buffer)); } - - encode(format, options = {}) { - if (format !== 'png') throw new RangeError('invalid image format'); - else return png.encode(this.u8, { channels: 4, width: this.width, height: this.height, level: ({ none: 0, fast: 3, default: 6, best: 9 })[options.compression] ?? 3 }); - } - - pixels(type) { - if ('rgba' === type) return ops.iterator.rgba(this); - if (!type || 'int' === type) return ops.iterator.u32(this); - - throw new RangeError('invalid iterator type'); - } - - flip(type) { - if (type === 'vertical') ops.flip.vertical(this); - else if (type === 'horizontal') ops.flip.horizontal(this); - - else throw new RangeError('invalid flip type'); - - return this; - } - - crop(type, arg0, arg1, arg2, arg3) { - if (type === 'circle') ops.crop.circle(arg0 || 0, this); - else if (type === 'box') ops.crop.crop(arg0 | 0, arg1 | 0, arg2 | 0, arg3 | 0, this); - - else throw new RangeError('invalid crop type'); - - return this; - } - - cut(type, arg0, arg1, arg2, arg3) { - if (type === 'circle') return ops.crop.circle(arg0 || 0, this.clone()); - else if (type === 'box') return ops.crop.cut(arg0 | 0, arg1 | 0, arg2 | 0, arg3 | 0, this); - - else throw new RangeError('invalid cut type'); - } - - rotate(deg, resize = true) { - if (0 === (deg %= 360)) return this; - else if (90 === deg) ops.rotate.rotate90(this); - else if (180 === deg) ops.rotate.rotate180(this); - else if (270 === deg) ops.rotate.rotate270(this); - - else ops.rotate.rotate(deg, this, resize); - - return this; - } - - blur(type, arg0) { - if (type === 'cubic') ops.blur.cubic(this); - else if (type === 'box') ops.blur.box(+arg0, this); - else if (type === 'gaussian') ops.blur.gaussian(+arg0, this); - - else throw new RangeError('invalid blur type'); - - return this; - } - - fill(color) { - const type = typeof color; - if (type === 'function') ops.fill.fn(color, this); - else if (type === 'number') ops.fill.color(color, this); - else if (color instanceof Color) ops.fill.color(color.valueOf(), this); - else if (Array.isArray(color)) ops.fill.color(ops.color.from_rgba(...color), this); - - else throw new TypeError('invalid fill color'); - - return this; - } - - swap(old, color) { - const ot = typeof old; - const nt = typeof color; - if (ot === nt && ot === 'number') ops.fill.swap(old, color, this); - else if (old instanceof Color && color instanceof Color) ops.fill.swap(old.valueOf(), color.valueOf(), this); - else if (Array.isArray(old) && Array.isArray(color)) ops.fill.swap(ops.color.from_rgba(...old), ops.color.from_rgba(...color), this); - - else throw new RangeError('invalid swap color'); - - return this; - } - - resize(type, width, height) { - if (width === this.width && height === this.height) return this; - else if (type === 'cubic') ops.resize.cubic(width, height, this); - else if (type === 'linear') ops.resize.linear(width, height, this); - else if (type === 'nearest') ops.resize.nearest(width, height, this); - - else throw new RangeError('invalid resize type'); - - return this; - } -} \ No newline at end of file diff --git a/v2/framebuffer.rs b/v2/framebuffer.rs deleted file mode 100644 index 64d8ad1..0000000 --- a/v2/framebuffer.rs +++ /dev/null @@ -1,1264 +0,0 @@ -#![allow(dead_code)] -#![warn(clippy::perf)] -#![feature(decl_macro)] -#![feature(const_mut_refs)] -#![feature(unchecked_math)] -#![warn(clippy::complexity)] -#![feature(core_intrinsics)] -#![warn(clippy::correctness)] -#![allow(non_camel_case_types)] -#![feature(downcast_unchecked)] -#![allow(non_upper_case_globals)] -#![feature(const_slice_from_raw_parts)] -#![feature(const_fn_floating_point_arithmetic)] - -macro size_of { ($type:ty) => { std::mem::size_of::<$type>() } } -#[inline] const fn alloc_len(size: usize) -> usize { return alloc_align(size).size(); } -#[inline(always)] const unsafe fn unreachable() -> ! { std::hint::unreachable_unchecked(); } -#[inline] fn alloc(size: usize) -> *mut u8 { unsafe { return std::alloc::alloc(alloc_align(size)); } } -#[inline] fn free(ptr: *mut u8, size: usize) { unsafe { std::alloc::dealloc(ptr, alloc_align(size)); } } -#[inline] fn calloc(size: usize) -> *mut u8 { unsafe { return std::alloc::alloc_zeroed(alloc_align(size)); } } -#[inline] const fn alloc_align(size: usize) -> std::alloc::Layout { return unsafe { std::alloc::Layout::from_size_align_unchecked(size, 16) }; } - -type fb = framebuffer; - -pub struct framebuffer { - pub width: usize, - pub height: usize, - ptr: (bool, *mut u8), -} - -unsafe impl Send for framebuffer {} -unsafe impl Sync for framebuffer {} - -pub trait framebuffer_from { - fn from(width: usize, height: usize, container: T) -> fb; -} - -impl Drop for framebuffer { - fn drop(&mut self) { if self.ptr.0 { free(self.as_mut_ptr(), self.len()); } } -} - -impl std::ops::Deref for framebuffer { - type Target = [u8]; fn deref(&self) -> &Self::Target { return unsafe { std::slice::from_raw_parts(self.as_ptr(), self.len()) }; } -} - -impl std::ops::DerefMut for framebuffer { - fn deref_mut(&mut self) -> &mut Self::Target { return unsafe { std::slice::from_raw_parts_mut(self.as_mut_ptr(), self.len()) }; } -} - -impl std::ops::IndexMut<(usize, usize)> for framebuffer { - fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output { return unsafe { &mut *(self.as_mut_ptr::().add(index.0 + index.1 * self.width) as *mut colors::rgba) }; } -} - -impl std::ops::Index<(usize, usize)> for framebuffer { - type Output = colors::rgba; fn index(&self, index: (usize, usize)) -> &Self::Output { return unsafe { &*(self.as_ptr::().add(index.0 + index.1 * self.width) as *mut colors::rgba) }; } -} - -impl Clone for framebuffer { - fn clone(&self) -> Self { - let ptr = alloc(self.len()); - unsafe { self.ptr.1.copy_to_nonoverlapping(ptr, self.len()); } - return Self { width: self.width, height: self.height, ptr: (true, ptr) }; - } -} - - -impl framebuffer_from<*mut T> for *mut T { fn from(width: usize, height: usize, container: *mut T) -> fb { return fb { width, height, ptr: (false, container as *mut u8) }; } } -impl framebuffer_from<&[T]> for &[T] { fn from(width: usize, height: usize, container: &[T]) -> fb { return fb { width, height, ptr: (false, container.as_ptr() as *mut u8) }; } } -impl framebuffer_from<*const T> for *const T { fn from(width: usize, height: usize, container: *const T) -> fb { return fb { width, height, ptr: (false, container as *mut u8) }; } } -impl framebuffer_from<&Vec> for &Vec { fn from(width: usize, height: usize, container: &Vec) -> fb { return fb { width, height, ptr: (false, container.as_ptr() as *mut u8) }; } } -impl framebuffer_from<&mut [T]> for &mut [T] { fn from(width: usize, height: usize, container: &mut [T]) -> fb { return fb { width, height, ptr: (false, container.as_mut_ptr() as *mut u8) }; } } -impl framebuffer_from<&mut Vec> for &mut Vec { fn from(width: usize, height: usize, container: &mut Vec) -> fb { return fb { width, height, ptr: (false, container.as_mut_ptr() as *mut u8) }; } } -impl framebuffer_from for colors::rgba { fn from(width: usize, height: usize, container: colors::rgba) -> fb { let mut fb = unsafe { framebuffer::new_uninit(width, height) }; ops::fill::color(&mut fb, container); return fb; } } -impl framebuffer_from for colors::rgb { fn from(width: usize, height: usize, container: colors::rgb) -> fb { let mut fb = unsafe { framebuffer::new_uninit(width, height) }; ops::fill::color(&mut fb, container.into()); return fb; } } -impl framebuffer_from for F where F: Fn(usize, usize) -> T { fn from(width: usize, height: usize, container: F) -> fb { let mut fb = unsafe { framebuffer::new_uninit(width, height) }; ops::fill::function(&mut fb, container); return fb; } } - - -impl framebuffer { - pub fn new(width: usize, height: usize) -> Self { return Self { width, height, ptr: (true, calloc(4 * width * height)) }; } - pub fn from>(width: usize, height: usize, container: T) -> Self { return T::from(width, height, container); } - pub unsafe fn new_uninit(width: usize, height: usize) -> Self { return Self { width, height, ptr: (true, alloc(4 * width * height)) }; } - - pub const fn len(&self) -> usize { return 4 * self.width * self.height; } - pub const fn as_ptr(&self) -> *const T { return self.ptr.1 as *const T; } - pub const fn as_mut_ptr(&mut self) -> *mut T { return self.ptr.1 as *mut T; } - pub const fn as_slice(&self) -> &[T] { return unsafe { std::slice::from_raw_parts(self.as_ptr(), self.len() / size_of!(T)) }; } - pub const fn as_mut_slice(&mut self) -> &mut [T] { return unsafe { std::slice::from_raw_parts_mut(self.as_mut_ptr(), self.len() / size_of!(T)) }; } - pub fn into_vec(mut self) -> Vec { self.ptr.0 = false; return unsafe { Vec::from_raw_parts(self.as_mut_ptr(), self.len() / size_of!(T), alloc_len(self.len()) / size_of!(T)) }; } -} - -pub mod colors { - #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct rgb { pub r: u8, pub g: u8, pub b: u8 } - #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct rgba { pub r: u8, pub g: u8, pub b: u8, pub a: u8 } - - impl From for rgb { #[inline] fn from(c: rgba) -> rgb { return rgb { r: c.r, g: c.g, b: c.b }; } } - impl From for rgba { #[inline] fn from(c: rgb) -> rgba { return rgba { r: c.r, g: c.g, b: c.b, a: 255 }; } } - impl From for u32 { #[inline] fn from(c: rgb) -> u32 { return c.r as u32 | ((c.g as u32) << 8) | ((c.b as u32) << 16) | (255 << 24); } } - impl From for u32 { #[inline] fn from(c: rgba) -> u32 { return c.r as u32 | ((c.g as u32) << 8) | ((c.b as u32) << 16) | ((c.a as u32) << 24); } } - impl From for rgb { #[inline] fn from(c: u32) -> rgb { return rgb { r: (c & 0xFF) as u8, g: ((c >> 8) & 0xFF) as u8, b: ((c >> 16) & 0xFF) as u8 }; } } - impl From for rgba { #[inline] fn from(c: u32) -> rgba { return rgba { r: (c & 0xFF) as u8, g: ((c >> 8) & 0xFF) as u8, b: ((c >> 16) & 0xFF) as u8, a: (c >> 24) as u8 }; } } - - pub fn blend(bg: u32, fg: u32) -> u32 { - let fa = fg >> 24; - let alpha = 1 + fa; - let inv_alpha = 256 - fa; - let r = (alpha * (fg & 0xff) + inv_alpha * (bg & 0xff)) >> 8; - let g = (alpha * ((fg >> 8) & 0xff) + inv_alpha * ((bg >> 8) & 0xff)) >> 8; - let b = (alpha * ((fg >> 16) & 0xff) + inv_alpha * ((bg >> 16) & 0xff)) >> 8; - return r | ((g & 0xff) << 8) | ((b & 0xff) << 16) | (fa.max(bg >> 24) << 24); - } -} - -pub mod ops { - use super::*; - - pub mod filter { - use super::*; - - pub fn opacity(fb: &mut fb, mut amount: f32) { - amount = amount.clamp(0.0, 1.0); - let u8 = unsafe { fb.as_mut_ptr::().offset(3) }; - - let mut offset = 0; - let len = fb.len(); - while len > offset { - use std::intrinsics::{fmul_fast as fm, float_to_int_unchecked as fi}; - unsafe { *u8.add(offset) = fi::(fm(amount, *u8.add(offset) as f32)); } - - offset += 4; - } - } - - pub fn brightness(fb: &mut fb, amount: f32) { - let u32 = fb.as_mut_ptr::(); - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fmul_fast as fm, float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let r = fi::(fm(amount, (c & 0xff) as f32)).min(255); - let g = fi::(fm(amount, ((c >> 8) & 0xff) as f32)).min(255); - let b = fi::(fm(amount, ((c >> 16) & 0xff) as f32)).min(255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn contrast(fb: &mut fb, amount: f32) { - let u32 = fb.as_mut_ptr::(); - let i = 255.0 * (0.5 - (0.5 * amount)); - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fadd_fast as fa, fmul_fast as fm, float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let r = fi::(fa(i, fm(amount, (c & 0xff) as f32))).min(255); - let g = fi::(fa(i, fm(amount, ((c >> 8) & 0xff) as f32))).min(255); - let b = fi::(fa(i, fm(amount, ((c >> 16) & 0xff) as f32))).min(255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn saturate(fb: &mut fb, amount: f32) { - let u32 = fb.as_mut_ptr::(); - - let filter: [f32; 9] = [ - 0.213 + 0.787 * amount, 0.715 - 0.715 * amount, 0.072 - 0.072 * amount, - 0.213 - 0.213 * amount, 0.715 + 0.285 * amount, 0.072 - 0.072 * amount, - 0.213 - 0.213 * amount, 0.715 - 0.715 * amount, 0.072 + 0.928 * amount, - ]; - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fadd_fast as fa, fmul_fast as fm, float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let rr = (c & 0xff) as f32; - let gg = ((c >> 8) & 0xff) as f32; - let bb = ((c >> 16) & 0xff) as f32; - let r = fi::(fa(fm(rr, filter[0]), fa(fm(gg, filter[1]), fm(bb, filter[2])))).clamp(0, 255); - let g = fi::(fa(fm(rr, filter[3]), fa(fm(gg, filter[4]), fm(bb, filter[5])))).clamp(0, 255); - let b = fi::(fa(fm(rr, filter[6]), fa(fm(gg, filter[7]), fm(bb, filter[8])))).clamp(0, 255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn sepia(fb: &mut fb, mut amount: f32) { - let u32 = fb.as_mut_ptr::(); - amount = (1.0 - amount).clamp(0.0, 1.0); - - let filter: [f32; 9] = [ - 0.393 + 0.607 * amount, 0.769 - 0.769 * amount, 0.189 - 0.189 * amount, - 0.349 - 0.349 * amount, 0.686 + 0.314 * amount, 0.168 - 0.168 * amount, - 0.272 - 0.272 * amount, 0.534 - 0.534 * amount, 0.131 + 0.869 * amount, - ]; - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fadd_fast as fa, fmul_fast as fm, float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let rr = (c & 0xff) as f32; - let gg = ((c >> 8) & 0xff) as f32; - let bb = ((c >> 16) & 0xff) as f32; - let r = fi::(fa(fm(rr, filter[0]), fa(fm(gg, filter[1]), fm(bb, filter[2])))).clamp(0, 255); - let g = fi::(fa(fm(rr, filter[3]), fa(fm(gg, filter[4]), fm(bb, filter[5])))).clamp(0, 255); - let b = fi::(fa(fm(rr, filter[6]), fa(fm(gg, filter[7]), fm(bb, filter[8])))).clamp(0, 255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn grayscale(fb: &mut fb, mut amount: f32) { - let u32 = fb.as_mut_ptr::(); - amount = (1.0 - amount).clamp(0.0, 1.0); - - let filter: [f32; 9] = [ - 0.2126 + 0.7874 * amount, 0.7152 - 0.7152 * amount, 0.0722 - 0.0722 * amount, - 0.2126 - 0.2126 * amount, 0.7152 + 0.2848 * amount, 0.0722 - 0.0722 * amount, - 0.2126 - 0.2126 * amount, 0.7152 - 0.7152 * amount, 0.0722 + 0.9278 * amount, - ]; - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fadd_fast as fa, fmul_fast as fm, float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let rr = (c & 0xff) as f32; - let gg = ((c >> 8) & 0xff) as f32; - let bb = ((c >> 16) & 0xff) as f32; - let r = fi::(fa(fm(rr, filter[0]), fa(fm(gg, filter[1]), fm(bb, filter[2])))).clamp(0, 255); - let g = fi::(fa(fm(rr, filter[3]), fa(fm(gg, filter[4]), fm(bb, filter[5])))).clamp(0, 255); - let b = fi::(fa(fm(rr, filter[6]), fa(fm(gg, filter[7]), fm(bb, filter[8])))).clamp(0, 255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn hue_rotate(fb: &mut fb, deg: f32) { - let u32 = fb.as_mut_ptr::(); - let cos = f32::cos(deg.to_radians()); - let sin = f32::sin(deg.to_radians()); - - let filter: [f32; 9] = [ - 0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928, - 0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283, - 0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072, - ]; - - for o in 0..(fb.len() / 4) { - unsafe { - use std::intrinsics::{fadd_fast as fa, fmul_fast as fm}; - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] const fn fi<_f, _i>(x: f32) -> u32 { return x as u32; } - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] use std::intrinsics::{float_to_int_unchecked as fi}; - - let c = *u32.add(o); - let rr = (c & 0xff) as f32; - let gg = ((c >> 8) & 0xff) as f32; - let bb = ((c >> 16) & 0xff) as f32; - let r = fi::(fa(fm(rr, filter[0]), fa(fm(gg, filter[1]), fm(bb, filter[2])))).clamp(0, 255); - let g = fi::(fa(fm(rr, filter[3]), fa(fm(gg, filter[4]), fm(bb, filter[5])))).clamp(0, 255); - let b = fi::(fa(fm(rr, filter[6]), fa(fm(gg, filter[7]), fm(bb, filter[8])))).clamp(0, 255); - - *u32.add(o) = r | (g << 8) | (b << 16) | (c >> 24 << 24); - } - } - } - - pub fn drop_shadow(fb: &mut fb, x: isize, y: isize, sigma: f32, color: Option) { - let mut old = fb.clone(); - let u32 = old.as_mut_ptr::(); - ops::blur::gaussian(&mut old, sigma); - - if color.is_some() { - unsafe { - let cc: u32 = color.unwrap_unchecked().into(); - - let ca = cc >> 24; - let cc = cc & 0xffffff; - - for o in 0..(fb.len() / 4) { - let c = *u32.add(o); - if 0 != (c >> 24) { *u32.add(o) = cc | (c >> 24 << 24) } - } - - if ca != 255 { ops::filter::opacity(&mut old, 1.0 / 255.0 * ca as f32); } - } - } - - ops::overlay::background(fb, &old, x, y); - } - - pub fn invert(fb: &mut fb, mut amount: f32) { - let u32 = fb.as_mut_ptr::(); - amount = amount.clamp(0.0, 1.0); - - if 1.0 == amount { - for o in 0..(fb.len() / 4) { - unsafe { - let c = *u32.add(o); - *u32.add(o) = !c & 0xffffff | (c >> 24 << 24); - } - } - } - - else { - let inv = 1.0 - amount; - - for o in 0..(fb.len() / 4) { - use std::intrinsics::{fadd_fast as fa, fsub_fast as fs, fmul_fast as fm, float_to_int_unchecked as fi}; - - unsafe { - let c = *u32.add(o); - let r = (c & 0xff) as f32; - let g = ((c >> 8) & 0xff) as f32; - let b = ((c >> 16) & 0xff) as f32; - let r = fi::(fa(fm(r, amount), fm(inv, fs(255.0, r)))); - let g = fi::(fa(fm(g, amount), fm(inv, fs(255.0, g)))); - let b = fi::(fa(fm(b, amount), fm(inv, fs(255.0, b)))); - *u32.add(o) = r | ((g & 0xff) << 8) | ((b & 0xff) << 16) | (c >> 24 << 24); - } - } - } - } - } - - pub mod crop { - use super::*; - use std::ops::Neg; - - pub fn r#box(fb: &fb, x: isize, y: isize, width: usize, height: usize) -> fb { - let old = fb; - let mut fb = fb::new(width, height); - ops::overlay::replace(&mut fb, old, x.neg(), y.neg()); - - return fb; - } - } - - pub mod flip { - use super::*; - - pub fn horizontal(fb: &mut fb) { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - for y in 0..height { unsafe { std::slice::from_raw_parts_mut(u32.add(y * width), width).reverse(); } } - } - - pub fn vertical(fb: &mut fb) { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - - for y in 0..(height / 2) { - let yoffset = y * width; - let yboffset = width * (height - 1 - y); - for x in 0..width { unsafe { std::ptr::swap(u32.add(x + yoffset), u32.add(x + yboffset)) }; } - } - } - } - - pub mod fill { - use super::*; - - pub trait fill_value { fn value(self) -> u32; } - impl fill_value for u32 { fn value(self) -> u32 { return self; } } - impl fill_value for colors::rgb { fn value(self) -> u32 { return self.into(); } } - impl fill_value for colors::rgba { fn value(self) -> u32 { return self.into(); } } - - pub fn function(fb: &mut fb, f: F) where F: Fn(usize, usize) -> T { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - - for y in 0..height { - let yoffset = y * width; - for x in 0..width { unsafe { *u32.add(x + yoffset) = f(x, y).value(); } } - } - } - - pub fn color(fb: &mut fb, color: colors::rgba) { - if color.r == color.g && color.r == color.b && color.r == color.a { unsafe { fb.as_mut_ptr::().write_bytes(color.r, fb.len()); } } - - else { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - unsafe { std::slice::from_raw_parts_mut(u32, width).fill(color.into()); } - for y in 1..height { unsafe { u32.copy_to_nonoverlapping(u32.add(y * width), width); } } - } - } - - pub fn background(fb: &mut fb, color: colors::rgba) { - let width = fb.width; - let bg = color.into(); - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - - for y in 0..height { - let yoffset = y * width; - - for x in 0..width { - unsafe { - let fg = *u32.add(x + yoffset); - - match (fg >> 24) & 0xff { - 0xff => {}, - 0x00 => *u32.add(x + yoffset) = bg, - _ => *u32.add(x + yoffset) = colors::blend(bg, fg), - } - } - } - } - } - } - - pub mod rotate { - use super::*; - - pub fn rotate180(fb: &mut fb) { - fb.as_mut_slice::().reverse(); - } - - pub fn rotate90(fb: &mut fb) { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - let o32 = alloc(fb.len()) as *mut u32; - unsafe { u32.copy_to_nonoverlapping(o32, fb.len() / 4); } - - - fb.width = height; - fb.height = width; - - for y in 0..height { - let yoffset = y * width; - let heighty1 = height - 1 - y; - for x in 0..width { unsafe { *u32.add(heighty1 + x * height) = *o32.add(x + yoffset); } } - } - - free(o32 as *mut u8, fb.len()); - } - - pub fn rotate270(fb: &mut fb) { - let width = fb.width; - let height = fb.height; - let u32 = fb.as_mut_ptr::(); - let o32 = alloc(fb.len()) as *mut u32; - unsafe { u32.copy_to_nonoverlapping(o32, fb.len() / 4); } - - fb.width = height; - fb.height = width; - - for y in 0..height { - let yoffset = y * width; - for x in 0..width { unsafe { *u32.add(y + height * (width - 1 - x)) = *o32.add(x + yoffset); } } - } - - free(o32 as *mut u8, fb.len()); - } - } - - pub mod overlay { - use super::*; - - pub fn replace(fb: &mut fb, fg: &fb, x: isize, y: isize) { - let f32 = fg.as_ptr::(); - let b32 = fb.as_mut_ptr::(); - let (bw, bh) = (fb.width as isize, fb.height as isize); - let (fw, fh) = (fg.width as isize, fg.height as isize); - - let top = y.max(0); - let left = x.max(0); - let ox = x.min(0).abs(); - let oy = y.min(0).abs(); - let width = bw.min(x + fw) - left; - let height = bh.min(y + fh) - top; - if 0 >= width || 0 >= height { return; } - - for yy in 0..height { - let yyoffset = ox + fw * (yy + oy); - let yoffset = left + bw * (yy + top); - unsafe { b32.offset(yoffset).copy_from(f32.offset(yyoffset), width as usize); } - } - } - - pub fn blend(fb: &mut fb, fg: &fb, x: isize, y: isize) { - let f32 = fg.as_ptr::(); - let b32 = fb.as_mut_ptr::(); - let (bw, bh) = (fb.width as isize, fb.height as isize); - let (fw, fh) = (fg.width as isize, fg.height as isize); - - let top = y.max(0); - let left = x.max(0); - let ox = x.min(0).abs(); - let oy = y.min(0).abs(); - let width = bw.min(x + fw) - left; - let height = bh.min(y + fh) - top; - if 0 >= width || 0 >= height { return; } - - for yy in 0..height { - let yyoffset = ox + fw * (yy + oy); - let yoffset = left + bw * (yy + top); - - for xx in 0..width { - unsafe { - let fg = *f32.offset(xx + yyoffset); - - match fg >> 24 { - 0x00 => {}, - 0xff => *b32.offset(xx + yoffset) = fg, - - fa => { - let alpha = 1 + fa; - let inv_alpha = 256 - fa; - let bg = *b32.offset(xx + yoffset); - let r = (alpha * (fg & 0xff) + inv_alpha * (bg & 0xff)) >> 8; - let g = (alpha * ((fg >> 8) & 0xff) + inv_alpha * ((bg >> 8) & 0xff)) >> 8; - let b = (alpha * ((fg >> 16) & 0xff) + inv_alpha * ((bg >> 16) & 0xff)) >> 8; - *b32.offset(xx + yoffset) = r | ((g & 0xff) << 8) | ((b & 0xff) << 16) | (fa.max(bg >> 24) << 24); - }, - } - } - } - } - } - - pub fn background(fb: &mut fb, bg: &fb, x: isize, y: isize) { - let bb32 = bg.as_ptr::(); - let b32 = fb.as_mut_ptr::(); - let (bw, bh) = (fb.width as isize, fb.height as isize); - let (fw, fh) = (bg.width as isize, bg.height as isize); - - let top = y.max(0); - let left = x.max(0); - let ox = x.min(0).abs(); - let oy = y.min(0).abs(); - let width = bw.min(x + fw) - left; - let height = bh.min(y + fh) - top; - if 0 >= width || 0 >= height { return; } - - for yy in 0..height { - let yyoffset = ox + fw * (yy + oy); - let yoffset = left + bw * (yy + top); - - for xx in 0..width { - unsafe { - let fg = *b32.offset(xx + yoffset); - - match fg >> 24 { - 0xff => {}, - 0x00 => *b32.offset(xx + yoffset) = *bb32.offset(xx + yyoffset), - - fa => { - let alpha = 1 + fa; - let inv_alpha = 256 - fa; - let bg = *bb32.offset(xx + yyoffset); - let r = (alpha * (fg & 0xff) + inv_alpha * (bg & 0xff)) >> 8; - let g = (alpha * ((fg >> 8) & 0xff) + inv_alpha * ((bg >> 8) & 0xff)) >> 8; - let b = (alpha * ((fg >> 16) & 0xff) + inv_alpha * ((bg >> 16) & 0xff)) >> 8; - *b32.offset(xx + yoffset) = r | ((g & 0xff) << 8) | ((b & 0xff) << 16) | (fa.max(bg >> 24) << 24); - }, - } - } - } - } - } - } - - pub mod resize { - use super::*; - - const fn lerp(a: u8, b: u8, t: f64) -> u8 { - return ((t * b as f64) + a as f64 * (1.0 - t)) as u8; - } - - fn clamped(x: usize, y: usize, width: usize, height: usize) -> usize { - return 4 * (x.clamp(0, width - 1) + width * y.clamp(0, height - 1)); - } - - const fn hermite(a: f64, b: f64, c: f64, d: f64, t: f64) -> f64 { - let cc = (c / 2.0) + (a / -2.0); - let bb = a + (c * 2.0) - (d / 2.0) - (b * 2.5); - let aa = (d / 2.0) + (a / -2.0) + (b * 1.5) - (c * 1.5); - - let t2 = t * t; - return b + (t * cc) + (bb * t2) + (t * aa * t2); - } - - pub fn nearest(fb: &fb, width: usize, height: usize) -> fb { - let owidth = fb.width; - let oheight = fb.height; - let o32 = fb.as_ptr::(); - let mut fb = unsafe { framebuffer::new_uninit(width, height) }; - - let u32 = fb.as_mut_ptr::(); - let xw = 1.0 / width as f64 * owidth as f64; - let yw = 1.0 / height as f64 * oheight as f64; - - for y in 0..height { - let yoffset = y * width; - let yyoffset = owidth * (yw * y as f64) as usize; - for x in 0..width { unsafe { *u32.add(x + yoffset) = *o32.add(yyoffset + (xw * x as f64) as usize); } }; - }; - - return fb; - } - - pub unsafe fn linear(fb: &fb, width: usize, height: usize) -> fb { - let owidth = fb.width; - let oheight = fb.height; - let o8 = fb.as_ptr::(); - let mut fb = framebuffer::new(width, height); - - let mut offset = 0; - let u8 = fb.as_mut_ptr::(); - let width1 = 1.0 / (width - 1) as f64; - let height1 = 1.0 / (height - 1) as f64; - - for y in 0..height { - let yy = oheight as f64 * (y as f64 * height1) - 0.5; - - let yyi = yy as usize; - let ty = yy - yyi as f64; - - for x in 0..width { - let xx = owidth as f64 * (x as f64 * width1) - 0.5; - - let xxi = xx as usize; - let tx = xx - xxi as f64; - - let s0 = clamped(xxi, yyi, owidth, oheight); - let s1 = clamped(1 + xxi, yyi, owidth, oheight); - let s2 = clamped(xxi, 1 + yyi, owidth, oheight); - let s3 = clamped(1 + xxi, 1 + yyi, owidth, oheight); - - // unsafe { - *u8.offset(offset) = lerp(lerp(*o8.add(s0), *o8.add(s2), tx), lerp(*o8.add(s1), *o8.add(s3), tx), ty); - *u8.offset(1 + offset) = lerp(lerp(*o8.add(1 + s0), *o8.add(1 + s2), tx), lerp(*o8.add(1 + s1), *o8.add(1 + s3), tx), ty); - *u8.offset(2 + offset) = lerp(lerp(*o8.add(2 + s0), *o8.add(2 + s2), tx), lerp(*o8.add(2 + s1), *o8.add(2 + s3), tx), ty); - *u8.offset(3 + offset) = lerp(lerp(*o8.add(3 + s0), *o8.add(3 + s2), tx), lerp(*o8.add(3 + s1), *o8.add(3 + s3), tx), ty); - // } - - offset += 4; - }; - }; - - return fb; - } - - pub unsafe fn cubic(fb: &fb, width: usize, height: usize) -> fb { - let owidth = fb.width; - let oheight = fb.height; - let o8 = fb.as_ptr::(); - let mut fb = framebuffer::new(width, height); - - let mut offset = 0; - let u8 = fb.as_mut_ptr::(); - let width1 = 1.0 / (width - 1) as f64; - let height1 = 1.0 / (height - 1) as f64; - - for y in 0..height { - let yy = oheight as f64 * (y as f64 * height1) - 0.5; - - let yyi = yy as usize; - let ty = yy - yyi as f64; - - for x in 0..width { - let xx = owidth as f64 * (x as f64 * width1) - 0.5; - - let xxi = xx as usize; - let tx = xx - xxi as f64; - - let s0 = clamped(xxi - 1, yyi - 1, owidth, oheight); - - let s1 = clamped(xxi, yyi - 1, owidth, oheight); - let s2 = clamped(1 + xxi, yyi - 1, owidth, oheight); - let s3 = clamped(2 + xxi, yyi - 1, owidth, oheight); - - let s4 = clamped(xxi - 1, yyi, owidth, oheight); - - let s5 = clamped(xxi, yyi, owidth, oheight); - let s6 = clamped(1 + xxi, yyi, owidth, oheight); - let s7 = clamped(2 + xxi, yyi, owidth, oheight); - - let s8 = clamped(xxi - 1, 1 + yyi, owidth, oheight); - - let s9 = clamped(xxi, 1 + yyi, owidth, oheight); - let s10 = clamped(1 + xxi, 1 + yyi, owidth, oheight); - let s11 = clamped(2 + xxi, 1 + yyi, owidth, oheight); - - let s12 = clamped(xxi - 1, 2 + yyi, owidth, oheight); - - let s13 = clamped(xxi, 2 + yyi, owidth, oheight); - let s14 = clamped(1 + xxi, 2 + yyi, owidth, oheight); - let s15 = clamped(2 + xxi, 2 + yyi, owidth, oheight); - - // unsafe { - let c0 = hermite(*o8.add(s0) as f64, *o8.add(s1) as f64, *o8.add(s2) as f64, *o8.add(s3) as f64, tx); - let c00 = hermite(*o8.add(1 + s0) as f64, *o8.add(1 + s1) as f64, *o8.add(1 + s2) as f64, *o8.add(1 + s3) as f64, tx); - let c000 = hermite(*o8.add(2 + s0) as f64, *o8.add(2 + s1) as f64, *o8.add(2 + s2) as f64, *o8.add(2 + s3) as f64, tx); - let c0000 = hermite(*o8.add(3 + s0) as f64, *o8.add(3 + s1) as f64, *o8.add(3 + s2) as f64, *o8.add(3 + s3) as f64, tx); - - let c1 = hermite(*o8.add(s4) as f64, *o8.add(s5) as f64, *o8.add(s6) as f64, *o8.add(s7) as f64, tx); - let c11 = hermite(*o8.add(1 + s4) as f64, *o8.add(1 + s5) as f64, *o8.add(1 + s6) as f64, *o8.add(1 + s7) as f64, tx); - let c111 = hermite(*o8.add(2 + s4) as f64, *o8.add(2 + s5) as f64, *o8.add(2 + s6) as f64, *o8.add(2 + s7) as f64, tx); - let c1111 = hermite(*o8.add(3 + s4) as f64, *o8.add(3 + s5) as f64, *o8.add(3 + s6) as f64, *o8.add(3 + s7) as f64, tx); - - let c2 = hermite(*o8.add(s8) as f64, *o8.add(s9) as f64, *o8.add(s10) as f64, *o8.add(s11) as f64, tx); - let c22 = hermite(*o8.add(1 + s8) as f64, *o8.add(1 + s9) as f64, *o8.add(1 + s10) as f64, *o8.add(1 + s11) as f64, tx); - let c222 = hermite(*o8.add(2 + s8) as f64, *o8.add(2 + s9) as f64, *o8.add(2 + s10) as f64, *o8.add(2 + s11) as f64, tx); - let c2222 = hermite(*o8.add(3 + s8) as f64, *o8.add(3 + s9) as f64, *o8.add(3 + s10) as f64, *o8.add(3 + s11) as f64, tx); - - let c3 = hermite(*o8.add(s12) as f64, *o8.add(s13) as f64, *o8.add(s14) as f64, *o8.add(s15) as f64, tx); - let c33 = hermite(*o8.add(1 + s12) as f64, *o8.add(1 + s13) as f64, *o8.add(1 + s14) as f64, *o8.add(1 + s15) as f64, tx); - let c333 = hermite(*o8.add(2 + s12) as f64, *o8.add(2 + s13) as f64, *o8.add(2 + s14) as f64, *o8.add(2 + s15) as f64, tx); - let c3333 = hermite(*o8.add(3 + s12) as f64, *o8.add(3 + s13) as f64, *o8.add(3 + s14) as f64, *o8.add(3 + s15) as f64, tx); - - *u8.offset(offset) = hermite(c0, c1, c2, c3, ty) as u8; - *u8.offset(1 + offset) = hermite(c00, c11, c22, c33, ty) as u8; - *u8.offset(2 + offset) = hermite(c000, c111, c222, c333, ty) as u8; - *u8.offset(3 + offset) = hermite(c0000, c1111, c2222, c3333, ty) as u8; - // } - - offset += 4; - }; - }; - - return fb; - } - } - - pub mod blur { - use super::*; - - pub fn gaussian(fb: &mut fb, sigma: f32) { - if 0.0 == sigma { return; } - - let cof = { - let a = (0.726f32).powi(2).exp() / sigma; - - let l = (-a).exp(); - let c = (-a * 2.0).exp(); - let k = (1.0 - l).powi(2) / (1.0 - c + a * l * 2.0); - - let a3 = c * -k; - let b1 = l * 2.0; - let a1 = l * k * (a - 1.0); - let a2 = l * k * (a + 1.0); - (k, a1, a2, a3, b1, -c, (k + a1) / (c - b1 + 1.0), (a2 + a3) / (c - b1 + 1.0)) - }; - - let width = fb.width; - let height = fb.height; - - unsafe { - let o8 = alloc(fb.len()); - let u8 = fb.as_mut_ptr::(); - let f32 = alloc(4 * 4 * width.max(height)) as *mut f32; - - u8.copy_to(o8, fb.len()); - gc(o8, u8, f32, width, height, cof); - gc(u8, o8, f32, height, width, cof); - - free(o8, fb.len()); - free(f32 as *mut u8, 4 * 4 * width.max(height)); - } - } - - unsafe fn gc(u8: *mut u8, o8: *mut u8, f32: *mut f32, width: usize, height: usize, (k, a1, a2, a3, b1, b2, lc, rc): (f32, f32, f32, f32, f32, f32, f32, f32)) { - use std::intrinsics::{fmul_fast as fm, fadd_fast as fa, float_to_int_unchecked as fi}; - - let width4 = 4 * width; - let height4 = 4 * height; - let hw1 = height * (width - 1); - - for y in 0..height { - let mut toffset = 0; - let mut ooffset = y * width4; - let mut offset = 4 * (y + hw1); - - let (mut por, mut pog, mut pob, mut poa) = (*o8.add(ooffset) as f32, *o8.add(1 + ooffset) as f32, *o8.add(2 + ooffset) as f32, *o8.add(3 + ooffset) as f32); - let (mut fur, mut fug, mut fub, mut fua) = (fm(lc, por), fm(lc, pog), fm(lc, pob), fm(lc, poa)); let (mut tur, mut tug, mut tub, mut tua) = (fur, fug, fub, fua); - - for _ in 0..width { - let (cor, cog, cob, coa) = (*o8.add(ooffset) as f32, *o8.add(1 + ooffset) as f32, *o8.add(2 + ooffset) as f32, *o8.add(3 + ooffset) as f32); - let (cur, cug, cub, cua) = (fm(k, cor) + fm(a1, por) + fm(b1, fur) + fm(b2, tur), fm(k, cog) + fm(a1, pog) + fm(b1, fug) + fm(b2, tug), fm(k, cob) + fm(a1, pob) + fm(b1, fub) + fm(b2, tub), fm(k, coa) + fm(a1, poa) + fm(b1, fua) + fm(b2, tua)); - - (tur, tug, tub, tua) = (fur, fug, fub, fua); - (fur, fug, fub, fua) = (cur, cug, cub, cua); - (por, pog, pob, poa) = (cor, cog, cob, coa); - - *f32.offset(toffset) = fur; - *f32.offset(1 + toffset) = fug; - *f32.offset(2 + toffset) = fub; - *f32.offset(3 + toffset) = fua; - - ooffset += 4; - toffset += 4; - } - - ooffset -= 4; - toffset -= 4; - - por = *o8.add(ooffset) as f32; - pog = *o8.add(1 + ooffset) as f32; - pob = *o8.add(2 + ooffset) as f32; - poa = *o8.add(3 + ooffset) as f32; - (tur, tug, tub, tua) = (fm(rc, por), fm(rc, pog), fm(rc, pob), fm(rc, poa)); - - (fur, fug, fub, fua) = (tur, tug, tub, tua); - let (mut cor, mut cog, mut cob, mut coa) = (por, pog, pob, poa); - - for _ in 0..width { - let (cur, cug, cub, cua) = (fm(a2, cor) + fm(a3, por) + fm(b1, fur) + fm(b2, tur), fm(a2, cog) + fm(a3, pog) + fm(b1, fug) + fm(b2, tug), fm(a2, cob) + fm(a3, pob) + fm(b1, fub) + fm(b2, tub), fm(a2, coa) + fm(a3, poa) + fm(b1, fua) + fm(b2, tua)); - - (tur, tug, tub, tua) = (fur, fug, fub, fua); - (fur, fug, fub, fua) = (cur, cug, cub, cua); - (por, pog, pob, poa) = (cor, cog, cob, coa); - - cor = *o8.add(ooffset) as f32; - cog = *o8.add(1 + ooffset) as f32; - cob = *o8.add(2 + ooffset) as f32; - coa = *o8.add(3 + ooffset) as f32; - *u8.add(offset) = fi(fa(fur, *f32.offset(toffset))); - *u8.add(1 + offset) = fi(fa(fug, *f32.offset(1 + toffset))); - *u8.add(2 + offset) = fi(fa(fub, *f32.offset(2 + toffset))); - *u8.add(3 + offset) = fi(fa(fua, *f32.offset(3 + toffset))); - - ooffset = ooffset.saturating_sub(4); - toffset = toffset.saturating_sub(4); - offset = offset.saturating_sub(height4); - } - } - } - } -} - -pub mod quant { - use super::*; - - struct bin { r: u32, g: u32, b: u32, a: u32, count: u32 } - - struct tbin { - n: u32, p: u32, - r: f32, g: f32, b: f32, a: f32, - la: u32, lb: u32, error: f32, count: f32, nearest: u32, - } - - pub struct result { - pub index: Vec, - pub palette: Vec, - pub transparency: Option, - } - - pub struct options { - pub fast: bool, - pub dither: bool, - pub colors: usize, - pub transparency: bool, - pub threshold: Option, - pub force_transparency: bool, - } - - impl Default for options { - fn default() -> Self { - return Self { - fast: false, - colors: 256, - dither: true, - threshold: None, - transparency: true, - force_transparency: false, - }; - } - } - - unsafe fn index(fb: &fb, _colors: usize, opt: &options, palette: &[colors::rgba], s: &(f32, f32, f32, f32)) -> Vec { - let mut index: Vec = Vec::with_capacity(fb.width * fb.height); - - let u8 = index.as_mut_ptr(); - let u32 = fb.as_ptr::(); - index.set_len(fb.width * fb.height); - let mut nearest: fnv::FnvHashMap = fnv::FnvHashMap::default(); - let mut closest: fnv::FnvHashMap = fnv::FnvHashMap::default(); - - if opt.fast { for o in 0..(fb.len() / 4) { *u8.add(o) = self::nearest(*u32.add(o), &palette, &s, &mut nearest); } } - else { for o in 0..(fb.len() / 4) { *u8.add(o) = self::closest(*u32.add(o), o, &palette, &s, &mut nearest, &mut closest); } } - - return index; - } - - unsafe fn link_neareast(o: usize, bins: *mut tbin, s: &(f32, f32, f32, f32)) { - let mut nearest = 0; - let mut e = f32::INFINITY; - let bin = &mut *bins.add(o); - - let n1 = bin.count; - let mut offset = bin.n; - let w = (bin.r, bin.g, bin.b, bin.a); - - while 0 != offset { - let b = &*bins.add(offset as usize); - - let mut err = - s.0 * (b.r - w.0).powi(2) - + s.1 * (b.g - w.1).powi(2) - + s.2 * (b.b - w.2).powi(2) - + s.3 * (b.a - w.3).powi(2); - - let n2 = b.count; - err *= (n1 * n2) / (n1 + n2); - if e > err { e = err; nearest = offset; } - offset = (*bins.offset(offset as isize)).n; - } - - bin.error = e; - bin.nearest = nearest; - } - - unsafe fn nearest(c: u32, palette: &[colors::rgba], s: &(f32, f32, f32, f32), cache: &mut fnv::FnvHashMap) -> u8 { - let old = cache.get(&c); - if old.is_some() { return *old.unwrap_unchecked(); } - - let a = (c >> 24) as f32; - let r = (c & 0xff) as f32; - let g = ((c >> 8) & 0xff) as f32; - let b = ((c >> 16) & 0xff) as f32; - - let mut offset = 0; - let p = palette.as_ptr(); - let mut m = f32::INFINITY; - - for o in 0..palette.len() { - let cc = *p.add(o); - let rr = cc.r as f32; - let gg = cc.g as f32; - let bb = cc.b as f32; - let aa = cc.a as f32; - let mut d = s.3 * (aa - a).powi(2); - - if d > m { continue; } - d += s.0 * (rr - r).powi(2); if d > m { continue; } - d += s.1 * (gg - g).powi(2); if d > m { continue; } - d += s.2 * (bb - b).powi(2); if d > m { continue; } - - m = d; offset = o as u8; - } - - cache.insert(c, offset); return offset; - } - - unsafe fn closest(c: u32, o: usize, palette: &[colors::rgba], s: &(f32, f32, f32, f32), nearest: &mut fnv::FnvHashMap, cache: &mut fnv::FnvHashMap) -> u8 { - let old = cache.get(&c); - - let m = if old.is_some() { *old.unwrap_unchecked() } else { - let a = (c >> 24) as f32; - let r = (c & 0xff) as f32; - let pp = palette.as_ptr(); - let g = ((c >> 8) & 0xff) as f32; - let b = ((c >> 16) & 0xff) as f32; - let mut m = (0u8, 0u8, f32::INFINITY, f32::INFINITY); - - for o in 0..palette.len() { - let cc = *pp.add(o); - let rr = cc.r as f32; - let gg = cc.g as f32; - let bb = cc.b as f32; - let aa = cc.a as f32; - let err = s.0 * (rr - r).powi(2) + s.1 * (gg - g).powi(2) + s.2 * (bb - b).powi(2) + s.3 * (aa - a).powi(2); - if m.2 > err { m.1 = m.0; m.3 = m.2; m.2 = err; m.0 = o as u8; } else if m.3 > err { m.3 = err; m.1 = o as u8; } - } - - if m.3 == f32::INFINITY { m.1 = m.0; } cache.insert(c, m); m - }; - - let l = palette.len(); - let mut oo = (1 + o) % 2; - if m.3 * 0.67 < (m.3 - m.2) { oo = 0 } else if m.0 > m.1 { oo = o % 2; } - - let err = match oo { 0 => m.2, 1 => m.3, _ => unreachable() }; - if err >= l as f32 { return self::nearest(c, palette, s, nearest); } - - return match oo { 0 => m.0, 1 => m.1, _ => unreachable() }; - } - - pub fn quantize(fb: &fb, options: &options) -> result { - let width = fb.width; - let height = fb.height; - let mut fb = fb.clone(); - let u32 = fb.as_mut_ptr::(); - let colors = options.colors.clamp(2, 256); - let mut s = (0.2126, 0.7152, 0.0722, 0.3333); - - if 32 >= colors { s = (1.0, 1.0, 1.0, 1.0); } - else if 512 > width || 512 > height { s = (0.299, 0.587, 0.114, s.3); } - - unsafe { - if !options.transparency { - s.3 = 0.0; - let threshold = options.threshold.unwrap_or(0x42) as u32; - - for o in 0..(fb.len() / 4) { - let a = *u32.add(o) >> 24; - *u32.add(0) = (if a < threshold { 0 } else { *u32.add(o) & 0xffffff }) | (255 << 24); - } - } else { - if options.threshold.is_none() { - for o in 0..(fb.len() / 4) { - let a = *u32.add(o) >> 24; - if a == 0 { *u32.add(0) = 0xffffff; } - } - } else { - let threshold = options.threshold.unwrap_unchecked() as u32; - - for o in 0..(fb.len() / 4) { - let a = *u32.add(o) >> 24; - *u32.add(0) = if a < threshold { 0xffffff } else { *u32.add(o) & 0xffffff | (255 << 24) }; - } - } - } - } - - let mut palette: Vec = vec![0.into(); colors]; - - if 2 < colors { - let (mut s, p) = unsafe { self::palette(&fb, colors, &options, &mut s) }; - - unsafe { - let ss = s; - let mut t = -1; - for o in 0..s { if 0 == (*p.add(o) >> 24) { t = o as isize; break; } } - if t == -1 && options.transparency && options.force_transparency { *p.add(if s == colors { s - 1 } else { s += 1; s - 1 }) = 0; } - - palette.as_mut_ptr().copy_from_nonoverlapping(p as _, s); palette.set_len(s); - - free(p as *mut u8, ss); - palette.sort_by(|&a, &b| u32::cmp(&b.into(), &a.into())); - } - } else { - unsafe { - *palette.get_unchecked_mut(0) = colors::rgba { r: 0, g: 0, b: 0, a: 255 }; - - let mut t = options.transparency && options.force_transparency; - if !t && options.transparency { for o in 0..(fb.len() / 4) { if 0 == (*u32.add(o) >> 24) { t = true; break; } } } - - *palette.get_unchecked_mut(1) = - if t { colors::rgba { r: 255, g: 255, b: 255, a: 0 } } - else { colors::rgba { r: 255, g: 255, b: 255, a: 255 } }; - } - } - - let mut t = None; - let index = unsafe { index(&fb, colors, &options, &palette, &s) }; - for o in 0..palette.len() { if 0 == palette[o].a { t = Some(o); break; } } - - return result { - index, - palette, - transparency: t, - }; - } - - unsafe fn palette(fb: &fb, colors: usize, options: &options, s: &mut (f32, f32, f32, f32)) -> (usize, *mut u32) { - let u32 = fb.as_ptr::(); - const BINS: usize = 2usize.pow(16); - - let (size, bins): (usize, *mut tbin) = { - let bins = calloc(BINS * size_of!(bin)) as *mut bin; - - let index = match options.fast { - true => |r: u32, g: u32, b: u32, a: u32| ((r & 0xF0) << 4) | (g & 0xF0) | (b >> 4) | ((a & 0xF0) << 8), - - false => match 64 <= colors && !options.transparency { - true => |r: u32, g: u32, b: u32, _: u32| ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3), - false => |r: u32, g: u32, b: u32, a: u32| ((r & 0xf8) << 7) | ((g & 0xf8) << 2) | (b >> 3) | ((a & 0x80) << 8), - }, - }; - - for o in 0..(fb.len() / 4) { - let c = *u32.add(o); - - let a = c >> 24; - let r = c & 0xff; - let g = (c >> 8) & 0xff; - let b = (c >> 16) & 0xff; - - let mut bin = &mut *bins.add(index(r, g, b, a) as usize); - - bin.r += r; - bin.g += g; - bin.b += b; - bin.a += a; - bin.count += 1; - } - - let mut size = 0; - - for o in 0..BINS { - let bin = bins.add(o); - if 0 == (*bin).count { continue; } - bins.add(size).copy_from(bin, 1); size += 1; - } - - let tbin = calloc(size * size_of!(tbin)) as *mut tbin; - - for o in 0..size { - use std::intrinsics::{fdiv_fast as fd}; - - let bin = &*bins.add(o); - let tbin = &mut *tbin.add(o); - - let count = bin.count as f32; - - tbin.count = count; - tbin.r = fd(bin.r as f32, count); - tbin.g = fd(bin.g as f32, count); - tbin.b = fd(bin.b as f32, count); - tbin.a = fd(bin.a as f32, count); - } - - free(bins as *mut u8, BINS * size_of!(bin)); - - (size, tbin) - }; - - let q = { - let weight = colors as f32 / size as f32; - let mut e = if 16 <= colors { 1 } else { -1 }; - if 0.003 < weight && 0.005 > weight { e = 0; }; - - if s.1 < 1.0 && 0.025 > weight { - s.1 -= 3.0 * (0.025 + weight); - s.2 += 3.0 * (0.025 + weight); - } - - match e { - 0 => |count: f32| count, - -1 => |count: f32| count.cbrt() as u32 as f32, - - 1 => match 64 > colors { - true => |count: f32| count.sqrt(), - false => |count: f32| count.sqrt() as u32 as f32, - }, - - _ => unreachable(), - } - }; - - for o in 0..(size - 1) { - let bin = &mut *bins.add(o); - - bin.n = (o + 1) as u32; - bin.count = q(bin.count); - (*bins.add(o + 1)).p = o as u32; - } - - (*bins.add(size - 1)).count = q((*bins.add(size - 1)).count); - - let heap = calloc(4 * (1 + BINS)) as *mut u32; - - for o in 0..size { - link_neareast(o, bins, &s); - let err = (*bins.add(o)).error; - - *heap += 1; - let mut l = *heap; - - while 1 < l { - let h = *heap.add((l >> 1) as usize); - if err >= (*bins.add(h as usize)).error { break; } - - *heap.add(l as usize) = h; l >>= 1; - } - - *heap.add(l as usize) = o as u32; - } - - let ext = size as isize - colors as isize; - - for o in 0..ext { - let mut b; - - loop { - b = *heap.add(1); - let tb = &mut *bins.add(b as usize); - if (tb.la >= tb.lb) && (tb.la >= (*bins.add(tb.nearest as usize)).lb) { break; } - - if tb.lb == 0xffff { - *heap -= 1; - let h = 1 + *heap; - b = *heap.add(h as usize); - *heap.offset(1) = *heap.add(h as usize); - } else { link_neareast(b as usize, bins, &s); tb.la = o as u32; } - - let mut l = 1; - let err = (*bins.add(b as usize)).error; - - while l + l <= *heap { - let mut l2 = l + l; - if (l2 < *heap) && ((*bins.add(*heap.add(l2 as usize) as usize)).error > (*bins.add(*heap.add(1 + l2 as usize) as usize)).error) { l2 += 1 } - - let h = *heap.add(l2 as usize); - if err <= (*bins.add(h as usize)).error { break; } - - *heap.add(l as usize) = h; l = l2; - } - - *heap.add(l as usize) = b; - } - - let tb = &mut *bins.add(b as usize); - let nb = &mut *bins.add(tb.nearest as usize); - - let n1 = tb.count; - let n2 = nb.count; - let d = 1.0 / (n1 + n2); - tb.r = d * (n1 * tb.r + n2 * nb.r).round(); - tb.g = d * (n1 * tb.g + n2 * nb.g).round(); - tb.b = d * (n1 * tb.b + n2 * nb.b).round(); - tb.a = d * (n1 * tb.a + n2 * nb.a).round(); - - nb.lb = 0xffff; - tb.count += nb.count; - tb.lb = (o + 1) as u32; - - (*bins.add(nb.p as usize)).n = nb.n; - (*bins.add(nb.n as usize)).p = nb.p; - } - - free(heap as *mut u8, 4 * (1 + BINS)); - let s = if 0 > ext { size } else { colors }; - let palette = alloc(s * size_of!(colors::rgba)) as *mut colors::rgba; - - let mut w = 0; - let mut offset = 0; - - loop { - let r = (*bins.add(offset)).r.clamp(0.0, 255.0) as u8; - let g = (*bins.add(offset)).g.clamp(0.0, 255.0) as u8; - let b = (*bins.add(offset)).b.clamp(0.0, 255.0) as u8; - let a = (*bins.add(offset)).a.clamp(0.0, 255.0) as u8; - - *palette.add(w) = colors::rgba { r, g, b, a }; - - w += 1; - offset = (*bins.add(offset)).n as usize; - - if 0 == offset { break; } - } - - return (s, palette as *mut u32); - } -} \ No newline at end of file diff --git a/v2/index.d.ts b/v2/index.d.ts deleted file mode 100644 index 218e349..0000000 --- a/v2/index.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Color } from './framebuffer'; -import type framebuffer from './framebuffer'; - -export { Color }; -export function load(buffer: BufferSource): Image | Animation; - -type svg_options = { - /** image fit options */ - fit?: svg_fit_zoom | svg_fit_width | svg_fit_height, -}; - -type png_options = { - compression?: 'fast' | 'best' | 'default', - filter?: 'up' | 'sub' | 'none' | 'paeth' | 'average', -} - -type svg_fit_zoom = { - /** zoom by factor */ - zoom: number, - width?: undefined, - height?: undefined, -}; -type svg_fit_width = { - /** scale to width */ - width: number, - zoom?: undefined, - height?: undefined, -}; - -type svg_fit_height = { - /** scale to height */ - height: number, - zoom?: undefined, - width?: undefined, -}; - -export class Frame { - image: Image; - timestamp: number; - dispose: 'any' | 'keep' | 'previous' | 'background'; - - constructor(width: number, height: number, buffer?: Image | BufferSource); - static from(framebuffer: { width: number, height: number, u8?: BufferSource, timestamp?: number, buffer?: BufferSource, dispose: 'any' | 'keep' | 'previous' | 'background' }): Frame; - - clone(): this; - get width(): number; - get height(): number; -} - -// @ts-ignore -export class Image extends framebuffer { - clone(): this; - static from(framebuffer: { width: number, height: number, u8?: BufferSource, buffer?: BufferSource }): Image; - - encode(format: 'png', options?: png_options): Promise; - encode(format: 'png', options: png_options & { sync: true }): Uint8Array; - encode(format: 'jpeg', options?: { quality?: number }): Promise; - encode(format: 'webp', options?: { quality?: number }): Promise; - encode(format: 'jpeg', options: { quality?: number } & { sync: true }): Uint8Array; - encode(format: 'webp', options: { quality?: number } & { sync: true }): Uint8Array; - - static decode(format: 'png', buffer: BufferSource): Promise; - static decode(format: 'jpeg', buffer: BufferSource): Promise; - static decode(format: 'tiff', buffer: BufferSource): Promise; - static decode(format: 'auto', buffer: string | BufferSource, options?: any): Promise; - static decode(format: 'svg', buffer: string | BufferSource, options?: svg_options): Promise; -} - -export class Animation { - repeat: number; - readonly width: number; - readonly height: number; - readonly frames: Frame[]; - - constructor(width: number, height: number); - static decode(format: 'gif', buffer: BufferSource, options?: {}): Promise; - static decode(format: 'auto', buffer: BufferSource, options?: any): Promise; - - clear(): void; - add(frame: Frame): void; - [Symbol.iterator]: Generator; - resize(type: 'cubic' | 'linear' | 'nearest', width: number, height: number): void; - encode(format: 'gif', options?: { repeat?: number, comment?: string, application?: string }): Promise; -} \ No newline at end of file diff --git a/v2/index.js b/v2/index.js deleted file mode 100644 index 8bcde41..0000000 --- a/v2/index.js +++ /dev/null @@ -1,261 +0,0 @@ -const mem = require('../utils/mem.js'); -const png = require('../png/node.js'); -const magic = require('./codecs/magic.js'); -const codecs = require('../codecs/node/index.js'); -const { Color, default: framebuffer } = require('./framebuffer.js'); - -const wasm = { - svg: require('../wasm/node/svg.js'), - gif: require('../wasm/node/gif.js'), - jpeg: require('../wasm/node/jpeg.js'), - tiff: require('../wasm/node/tiff.js'), - // font: require('../wasm/node/font.js'), -} - -// todo: verbose range errors - -const animation_formats = ['gif']; -const image_formats = ['png', 'jpeg', 'tiff']; -const all_formats = ['png', 'gif', 'jpeg', 'tiff']; - -async function load(buffer) { - const u8 = mem.view(buffer); - if (0 === u8.length) throw new RangeError('empty buffer'); - - const meta = magic.buffer(u8); - if (!meta) throw new RangeError('unknown file format'); - if ('image' !== meta.type) throw new RangeError('unsupported file type'); - if (!all_formats.includes(meta.format)) throw new RangeError('unsupported image format'); - - if ('gif' === meta.format) return Animation.decode('gif', u8); - return Image.from('png' === meta.format ? png.decode(u8) : (await wasm[meta.format].init()).load(u8)); -} - -class Frame { - timestamp = 0; - dispose = 'any'; - - constructor(width, height, buffer) { - this.image = buffer instanceof Image ? buffer : new Image(width | 0, height | 0, buffer); - } - - get width() { return this.image.width; } - get height() { return this.image.height; } - - clone() { - const frame = new Frame(this.width, this.height, this.image.clone()); - - frame.dispose = this.dispose; - frame.timestamp = this.timestamp; - - return frame; - } - - static from(framebuffer) { - const frame = new Frame(framebuffer.width, framebuffer.height, framebuffer.u8 || framebuffer.buffer); - - frame.dispose = framebuffer.dispose || 'any'; - frame.timestamp = framebuffer.timestamp || 0; - - return frame; - } -} - -class Image extends framebuffer { - constructor(width, height, buffer) { - super(width, height, buffer || new Uint8Array(4 * width * height)); - } - - encode(format, options = {}) { - const method = options.sync ? 'encode' : 'encode_async'; - if ('png' === format) return codecs.png[method](this.u8, { ...options, width: this.width, height: this.height }); - if ('jpeg' === format) return codecs.jpeg[method](this.u8, { ...options, width: this.width, height: this.height }); - if ('webp' === format) return codecs.webp[method](this.u8, { ...options, width: this.width, height: this.height }); - - throw new RangeError('invalid image format'); - } - - static async decode(format, buffer, options = {}) { - buffer = mem.view('string' === typeof buffer ? Buffer.from(buffer) : buffer); - - if ('auto' === format) { - const meta = magic.buffer(buffer); - if (!meta) throw new RangeError('unknown file format'); - if ('image' !== meta.type) throw new RangeError('unsupported file type'); - if (!image_formats.includes(meta.format)) throw new RangeError('unsupported image format'); - - format = meta.format; - } - - let frame; - if ('svg' === format) frame = (await wasm.svg.init()).load(buffer, options.fit ?? null); - - else if (format in wasm) frame = (await wasm[format].init()).load(buffer); - - if (!frame) throw new RangeError('invalid image format'); - return new Image(frame.width, frame.height, frame.buffer); - } -} - -class Animation { - width = 0; - height = 0; - frames = []; - repeat = Infinity; - - constructor(width, height) { - this.width = width | 0; - this.height = height | 0; - } - - clear() { this.frames.length = 0; } - [Symbol.iterator]() { return this.frames.values(); } - - add(frame) { - if (this.width !== frame.width) throw new RangeError('invalid frame width'); - else if (this.height !== frame.height) throw new RangeError('invalid frame height'); - - this.frames.push(frame); - } - - resize(type, width, height) { - this.width = width | 0; - this.height = height | 0; - for (const frame of this) frame.image.resize(type, this.width, this.height); - } - - async encode(format, options = {}) { - if ('gif' === format) { - const quality = options.quality ?? 95; - const encoder = new codecs.gif.encoder(this.width, this.height); - - let prev = { timestamp: 0 }; - for (const frame of this.frames) { - encoder.add(frame.image.u8, { - x: 0, - y: 0, - quality, - width: frame.width, - speed: options.speed, - height: frame.height, - colors: options.colors, - dispose: frame.dispose, - delay: ((frame.timestamp - prev.timestamp) / 10) | 0, - }); - - prev = frame; - } - - const repeat = options.repeat ?? this.repeat; - return encoder.finish({ ...options, repeat: Infinity === repeat ? null : repeat }); - } - - throw new RangeError('invalid animation format'); - } - - static async decode(format, buffer, options = {}) { - buffer = mem.view(buffer); - - if ('auto' === format) { - const meta = magic.buffer(buffer); - if (!meta) throw new RangeError('unknown file format'); - if ('image' !== meta.type) throw new RangeError('unsupported file type'); - if (!animation_formats.includes(meta.format)) throw new RangeError('unsupported animation format'); - - format = meta.format; - } - - if ('gif' === format) { - const decoder = new (await wasm.gif.init()).Decoder(buffer); - - const gwidth = decoder.width | 0; - const gheight = decoder.height | 0; - const animation = new Animation(gwidth, gheight); - const u32 = new Uint32Array(decoder.width * decoder.height); - const u8 = new Uint8Array(u32.buffer, u32.byteOffset, u32.byteLength); - - let prev = { timestamp: 0 }; - const frames = animation.frames; - for (const frame of decoder.frames()) { - let offset8 = 0 | 0; - let offset32 = 0 | 0; - const fx = frame.x | 0; - const fy = frame.y | 0; - const f8 = frame.buffer; - const mode = frame.dispose; - const width = frame.width | 0; - const height = frame.height | 0; - const f = frames[frames.push(new Frame(gwidth, gheight)) - 1]; - const f32 = new Uint32Array(f8.buffer, f8.byteOffset, width * height); - - const t8 = f.image.u8; - f.dispose = 'background'; - const t32 = new Uint32Array(t8.buffer); - f.timestamp = prev.timestamp + 10 * frame.delay; - - prev = f; - t8.set(u8); - - if (2 === mode) { - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - } - } - } - - else if (3 === mode) { - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - u32[x_offset] = 0; - } - } - } - - else if (0 === mode || 1 === mode) { - t8.set(u8) - for (let y = 0 | 0; y < height; y++) { - const y_offset = fx + gwidth * (y + fy) | 0; - - for (let x = 0 | 0; x < width; x++) { - const x_offset = x + y_offset; - - if (0 === f8[3 + offset8]) - t32[x_offset] = u32[x_offset]; - else t32[x_offset] = f32[offset32]; - - offset32++; - offset8 += 4; - u32[x_offset] = t32[x_offset]; - } - } - } - } - - return animation; - } - - throw new RangeError('invalid animation format'); - } -} - -module.exports = { load, Image, Frame, Color, Animation }; diff --git a/v2/ops/blur.mjs b/v2/ops/blur.mjs deleted file mode 100644 index 86ba29d..0000000 --- a/v2/ops/blur.mjs +++ /dev/null @@ -1,303 +0,0 @@ -export function cubic(framebuffer) { - const width = framebuffer.width; - const height = framebuffer.height; - framebuffer.resize('cubic', Math.max(1, .008 * width), Math.max(1, .008 * height)); - - framebuffer.resize('cubic', width, height); -} - -export function box(radius, framebuffer) { - if (!radius) return; - const u8 = framebuffer.u8; - const width = framebuffer.width; - const height = framebuffer.height; - const old = framebuffer.u8.slice(); - bb(u8, old, width, height, radius); -} - -export function gaussian(radius, framebuffer) { - if (0 >= radius) return; - const a = Math.exp(.726 ** 2) / radius; - - const g1 = Math.exp(-a); - const g2 = Math.exp(a * -2); - const old = framebuffer.u8.slice(); - const k = ((1 - g1) ** 2) / (1 - g2 + 2 * a * g1); - - const b2 = -g2; - const b1 = 2 * g1; - const a3 = -k * g2; - const a1 = k * g1 * (a - 1); - const a2 = k * g1 * (a + 1); - const lc = (k + a1) / (1 - b1 - b2); - const width = framebuffer.width | 0; - const rc = (a2 + a3) / (1 - b1 - b2); - const height = framebuffer.height | 0; - const tmp = new Float32Array(4 * Math.max(width, height)); - gc(old, framebuffer.u8, tmp, width, height, k, a1, a2, a3, b1, b2, lc, rc); - gc(framebuffer.u8, old, tmp, height, width, k, a1, a2, a3, b1, b2, lc, rc); -} - -function bb(u8, old, width, height, radius) { - const divisor = 1 / (1 + radius + radius); - bbt(u8, old, width, height, radius, divisor); - bbh(u8, old, width, height, radius, divisor); -} - -function bbh(u8, old, width, height, radius, divisor) { - for (var y = 0; y < height; y++) { - let y_offset = y * width; - - let li = y_offset; - let ri = radius + y_offset; - const fv_offset = 4 * y_offset; - const lv_offset = 4 * (width - 1 + y_offset); - - const rfv = old[fv_offset]; - const gfv = old[1 + fv_offset]; - const bfv = old[2 + fv_offset]; - const afv = old[3 + fv_offset]; - - const rlv = old[lv_offset]; - const glv = old[1 + lv_offset]; - const blv = old[2 + lv_offset]; - const alv = old[3 + lv_offset]; - - let r = rfv * (1 + radius); - let g = gfv * (1 + radius); - let b = bfv * (1 + radius); - let a = afv * (1 + radius); - - for (let x = 0; x < radius; x++) { - const offset = 4 * (x + y_offset); - - r += old[offset]; - g += old[1 + offset]; - b += old[2 + offset]; - a += old[3 + offset]; - } - - for (let x = 0; x <= radius; x++) { - let offset = 4 * ri++; - r += old[offset] - rfv; - g += old[1 + offset] - gfv; - b += old[2 + offset] - bfv; - a += old[3 + offset] - afv; - - offset = 4 * y_offset++; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - } - - for (let x = 1 + radius; x < (width - radius); x++) { - let offset = 4 * ri++; - const roffset = 4 * li++; - r += old[offset] - old[roffset]; - g += old[1 + offset] - old[1 + roffset]; - b += old[2 + offset] - old[2 + roffset]; - a += old[3 + offset] - old[3 + roffset]; - // todo: how far is roffset - - offset = 4 * y_offset++; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - } - - for (let x = width - radius; x < width; x++) { - let offset = 4 * li++; - r += rlv - old[offset]; - g += glv - old[1 + offset]; - b += blv - old[2 + offset]; - a += alv - old[3 + offset]; - - offset = 4 * y_offset++; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - } - } -} - -function bbt(u8, old, width, height, radius, divisor) { - for (var x = 0; x < width; x++) { - let ti = x; - let li = ti; - const fv_offset = 4 * ti; - let ri = ti + width * radius; - const lv_offset = 4 * (ti + width * (height - 1)); - - const rfv = old[fv_offset]; - const gfv = old[1 + fv_offset]; - const bfv = old[2 + fv_offset]; - const afv = old[3 + fv_offset]; - - const rlv = old[lv_offset]; - const glv = old[1 + lv_offset]; - const blv = old[2 + lv_offset]; - const alv = old[3 + lv_offset]; - - let r = rfv * (1 + radius); - let g = gfv * (1 + radius); - let b = bfv * (1 + radius); - let a = afv * (1 + radius); - - for (let y = 0; y < radius; y++) { - const offset = 4 * (ti + y * width); - - r += old[offset]; - g += old[1 + offset]; - b += old[2 + offset]; - a += old[3 + offset]; - } - - for (let y = 0; y <= radius; y++) { - let offset = 4 * ri; - r += old[offset] - rfv; - g += old[1 + offset] - gfv; - b += old[2 + offset] - bfv; - a += old[3 + offset] - afv; - - offset = 4 * ti; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - - ri += width; - ti += width; - } - - for (let y = 1 + radius; y < (height - radius); y++) { - let offset = 4 * ri; - const xoffset = 4 * li; - r += old[offset] - old[xoffset]; - g += old[1 + offset] - old[1 + xoffset]; - b += old[2 + offset] - old[2 + xoffset]; - a += old[3 + offset] - old[3 + xoffset]; - // todo: how far is xoffset - - offset = 4 * ti; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - - li += width; - ri += width; - ti += width; - } - - for (let y = height - radius; y < height; y++) { - let offset = 4 * li; - r += rlv - old[offset]; - g += glv - old[1 + offset]; - b += blv - old[2 + offset]; - a += alv - old[3 + offset]; - - offset = 4 * ti; - u8[offset] = Math.round(r * divisor); - u8[1 + offset] = Math.round(g * divisor); - u8[2 + offset] = Math.round(b * divisor); - u8[3 + offset] = Math.round(a * divisor); - - li += width; - ti += width; - } - } -} - -function gc(u8, old, tmp, width, height, k, a1, a2, a3, b1, b2, lc, rc) { - const width4 = width * 4; - const height4 = height * 4; - const hw1 = height * (width - 1); - - for (let y = 0; y < height; y++) { - let toffset = 0 | 0; - let ooffset = (y * width4) | 0; - let offset = (4 * (y + hw1)) | 0; - - let por = old[ooffset]; - let pog = old[1 + ooffset]; - let pob = old[2 + ooffset]; - let poa = old[3 + ooffset]; - - let pur = lc * por; - let pug = lc * pog; - let pub = lc * pob; - let pua = lc * poa; - - let ppur = pur; - let ppug = pug; - let ppub = pub; - let ppua = pua; - - for (let x = 0; x < width; x++) { - const cor = old[ooffset++]; - const cog = old[ooffset++]; - const cob = old[ooffset++]; - const coa = old[ooffset++]; - - const cur = k * cor + a1 * por + b1 * pur + b2 * ppur; - const cug = k * cog + a1 * pog + b1 * pug + b2 * ppug; - const cub = k * cob + a1 * pob + b1 * pub + b2 * ppub; - const cua = k * coa + a1 * poa + b1 * pua + b2 * ppua; - - ppur = pur; pur = cur; por = cor; - ppug = pug; pug = cug; pog = cog; - ppub = pub; pub = cub; pob = cob; - ppua = pua; pua = cua; poa = coa; - - tmp[toffset++] = pur; - tmp[toffset++] = pug; - tmp[toffset++] = pub; - tmp[toffset++] = pua; - } - - ooffset -= 4; - toffset -= 4; - ppur = rc * (por = old[ooffset]); - ppug = rc * (pog = old[1 + ooffset]); - ppub = rc * (pob = old[2 + ooffset]); - ppua = rc * (poa = old[3 + ooffset]); - - pur = ppur; - pug = ppug; - pub = ppub; - pua = ppua; - let cor = por; - let cog = pog; - let cob = pob; - let coa = poa; - - for (let x = width - 1; 0 <= x; x--) { - const cur = a2 * cor + a3 * por + b1 * pur + b2 * ppur; - const cug = a2 * cog + a3 * pog + b1 * pug + b2 * ppug; - const cub = a2 * cob + a3 * pob + b1 * pub + b2 * ppub; - const cua = a2 * coa + a3 * poa + b1 * pua + b2 * ppua; - - ppur = pur; pur = cur; por = cor; - ppug = pug; pug = cug; pog = cog; - ppub = pub; pub = cub; pob = cob; - ppua = pua; pua = cua; poa = coa; - - cor = old[ooffset]; - cog = old[1 + ooffset]; - cob = old[2 + ooffset]; - coa = old[3 + ooffset]; - - u8[offset] = pur + tmp[toffset]; - u8[1 + offset] = pug + tmp[1 + toffset]; - u8[2 + offset] = pub + tmp[2 + toffset]; - u8[3 + offset] = pua + tmp[3 + toffset]; - - ooffset -= 4; - toffset -= 4; - offset -= height4; - } - } -} \ No newline at end of file diff --git a/v2/ops/color.mjs b/v2/ops/color.mjs deleted file mode 100644 index b528aac..0000000 --- a/v2/ops/color.mjs +++ /dev/null @@ -1,277 +0,0 @@ -const short_hex_regex = /^#?([\da-f]{3,4})$/; -const long_hex_regex = /^#?((?:[\da-f]{2}){3,4})$/; -const rgb_regex = /^rgba?\((?(?:\d*\.)?\d+)(?: +| *, *)(?(?:\d*\.)?\d+)(?: +| *, *)(?(?:\d*\.)?\d+)(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; -const rgb_percentage_regex = /^rgba?\((?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; -const hsl_regex = /^hsla?\((?(?:\d*\.)?\d+)(?|deg|rad|grad|turn)(?: +| *, *)(?(?:\d*\.)?\d+)%(?: +| *, *)(?(?:\d*\.)?\d+)%(?:(?: +| *, *)(?\d+|\d*\.\d+|\d+(?:\.\d+)?%))?\)$/; - -function clamp(min, max, int) { - return Math.min(Math.max(Math.round(int), min), max); -} - -export function to_rgba(int) { - return [(int >> 24) & 0xff, (int >> 16) & 0xff, (int >> 8) & 0xff, int & 0xff]; -} - -export function from_rgba(r, g, b, a) { - return ((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff); -} - -function parse_alpha(a) { - return clamp(0, 255, ('%' === a[a.length - 1]) ? ((255 / 100) * parseFloat(a)) : (+a * 255)); -} - -function hue_from_type(h, t) { - if (t === 'turn') return h / 1; - if (t === 'grad') return h / 400; - if (!t || t === 'deg') return h / 360; - if (t === 'rad') return h / (2 * Math.PI); -} - -function hue2rgb(p, q, t) { - if (t < 0) t += 1; - else if (t > 1) t -= 1; - if (t < 1 / 2) return q; - if (t < 1 / 6) return p + 6 * t * (q - p); - if (t < 2 / 3) return p + 6 * (q - p) * (2 / 3 - t); - - return p; -}; - -export function blend(fg, bg) { - const fa = fg & 0xff; - const alpha = fa + 1 | 0; - const inv_alpha = 256 - fa | 0; - const r = (alpha * (fg >>> 24) + inv_alpha * (bg >>> 24)) >> 8; - const b = (alpha * (fg >> 8 & 0xff) + inv_alpha * (bg >> 8 & 0xff)) >> 8; - const g = (alpha * (fg >> 16 & 0xff) + inv_alpha * (bg >> 16 & 0xff)) >> 8; - return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (Math.max(fa, bg & 0xff) & 0xff)); -} - -export function parse(any) { - let x = null; - if (undefined !== (x = colors.get(any))) return x; - if (x = long_hex_regex.exec(any)) return parseInt(`${x[1]}${8 === x[1].length ? '' : 'ff'}`, 16); - if (x = hsl_regex.exec(any)) return color.hsla(hue_from_type(x[1], x[2]), x[3] / 100, x[4] / 100, x[5] ? ((1 / 255) * parse_alpha(x[5])) : 1); - if (x = rgb_regex.exec(any)) return color.rgba(clamp(0, 255, +x[1]), clamp(0, 255, +x[2]), clamp(0, 255, +x[3]), x[4] ? parse_alpha(x[4]) : 255); - if (x = short_hex_regex.exec(any)) return parseInt(`${x[1][0]}${x[1][0]}${x[1][1]}${x[1][1]}${x[1][2]}${x[1][2]}${3 === x[1].length ? 'ff' : `${x[1][3]}${x[1][3]}`}`, 16); - if (x = rgb_percentage_regex.exec(any)) return color.rgba(clamp(0, 255, +x[1] * (255 / 100)), clamp(0, 255, +x[2] * (255 / 100)), clamp(0, 255, +x[3] * (255 / 100)), x[4] ? parse_alpha(x[4]) : 255); - - return null; -} - -export default class color { - constructor(any) { - this.value = parse(String(any).toLowerCase()); - if (null === this.value) throw new Error(`invalid css color (${any})`); - } - - static rgb(r, g, b) { - return this.rgba(r, g, b, 255); - } - - static rgba(r, g, b, a) { - return (((r & 0xff) << 24) | ((g & 0xff) << 16) | ((b & 0xff) << 8) | (a & 0xff)); - } - - static hsla(h, s, l, a) { - s = Math.min(1, Math.max(0, s)); - a = Math.min(1, Math.max(0, a)); - if (s === 0) return this.rgba(255, 255, 255, a * 255); - - h %= 1; - l = Math.min(1, Math.max(0, l)); - const q = l < .5 ? l + s * l : l + s - l * s; - - const p = 2 * l - q; - const g = hue2rgb(p, q, h); - const r = hue2rgb(p, q, h + 1 / 3); - const b = hue2rgb(p, q, h - 1 / 3); - return this.rgba(r * 255, g * 355, b * 255, a * 255); - } - - valueOf() { - return this.value; - } - - toJSON() { - return this.value >>> 0; - } - - get rgb() { - return [this.value >>> 24, this.value >> 16 & 0xff, this.value >> 8 & 0xff]; - } - - get rgba() { - return [this.value >>> 24, this.value >> 16 & 0xff, this.value >> 8 & 0xff, this.value & 0xff]; - } - - get name() { - for (const color of colors.keys()) { - if (this.value === colors.get(color)) return color; - } - - return null; - } - - toString(radix) { - const type = String(radix).toLowerCase(); - - const rgba = this.rgba; - if (type === 'rgb' || type === 'rgba') return `rgb${type[3] ? 'a' : ''}(${rgba[0]}, ${rgba[1]}, ${rgba[2]}${type[3] ? `, ${clamp(0, 100, 100 / 255 * rgba[3])}%` : ''})`; - if (type === '16' || type === 'hex') return `#${rgba[0].toString(16).padStart(2, '0')}${rgba[1].toString(16).padStart(2, '0')}${rgba[2].toString(16).padStart(2, '0')}${rgba[3] === 255 ? '' : rgba[3].toString(16).padStart(2, '0')}`; - - return this.value.toString(); - } -} - -const colors = new Map([ - ['aliceblue', 0xf0f8ffff], - ['antiquewhite', 0xfaebd7ff], - ['aqua', 0x00ffffff], - ['aquamarine', 0x7fffd4ff], - ['azure', 0xf0ffffff], - ['beige', 0xf5f5dcff], - ['bisque', 0xffe4c4ff], - ['black', 0x000000ff], - ['blanchedalmond', 0xffebcdff], - ['blue', 0x0000ffff], - ['blueviolet', 0x8a2be2ff], - ['brown', 0xa52a2aff], - ['burlywood', 0xdeb887ff], - ['cadetblue', 0x5f9ea0ff], - ['chartreuse', 0x7fff00ff], - ['chocolate', 0xd2691eff], - ['coral', 0xff7f50ff], - ['cornflowerblue', 0x6495edff], - ['cornsilk', 0xfff8dcff], - ['crimson', 0xdc143cff], - ['cyan', 0x00ffffff], - ['darkblue', 0x00008bff], - ['darkcyan', 0x008b8bff], - ['darkgoldenrod', 0xb8860bff], - ['darkgray', 0xa9a9a9ff], - ['darkgreen', 0x006400ff], - ['darkgrey', 0xa9a9a9ff], - ['darkkhaki', 0xbdb76bff], - ['darkmagenta', 0x8b008bff], - ['darkolivegreen', 0x556b2fff], - ['darkorange', 0xff8c00ff], - ['darkorchid', 0x9932ccff], - ['darkred', 0x8b0000ff], - ['darksalmon', 0xe9967aff], - ['darkseagreen', 0x8fbc8fff], - ['darkslateblue', 0x483d8bff], - ['darkslategray', 0x2f4f4fff], - ['darkslategrey', 0x2f4f4fff], - ['darkturquoise', 0x00ced1ff], - ['darkviolet', 0x9400d3ff], - ['deeppink', 0xff1493ff], - ['deepskyblue', 0x00bfffff], - ['dimgray', 0x696969ff], - ['dimgrey', 0x696969ff], - ['dodgerblue', 0x1e90ffff], - ['firebrick', 0xb22222ff], - ['floralwhite', 0xfffaf0ff], - ['forestgreen', 0x228b22ff], - ['fuchsia', 0xff00ffff], - ['gainsboro', 0xdcdcdcff], - ['ghostwhite', 0xf8f8ffff], - ['gold', 0xffd700ff], - ['goldenrod', 0xdaa520ff], - ['gray', 0x808080ff], - ['green', 0x008000ff], - ['greenyellow', 0xadff2fff], - ['grey', 0x808080ff], - ['honeydew', 0xf0fff0ff], - ['hotpink', 0xff69b4ff], - ['indianred', 0xcd5c5cff], - ['indigo', 0x4b0082ff], - ['ivory', 0xfffff0ff], - ['khaki', 0xf0e68cff], - ['lavender', 0xe6e6faff], - ['lavenderblush', 0xfff0f5ff], - ['lawngreen', 0x7cfc00ff], - ['lemonchiffon', 0xfffacdff], - ['lightblue', 0xadd8e6ff], - ['lightcoral', 0xf08080ff], - ['lightcyan', 0xe0ffffff], - ['lightgoldenrodyellow', 0xfafad2ff], - ['lightgray', 0xd3d3d3ff], - ['lightgreen', 0x90ee90ff], - ['lightgrey', 0xd3d3d3ff], - ['lightpink', 0xffb6c1ff], - ['lightsalmon', 0xffa07aff], - ['lightseagreen', 0x20b2aaff], - ['lightskyblue', 0x87cefaff], - ['lightslategray', 0x778899ff], - ['lightslategrey', 0x778899ff], - ['lightsteelblue', 0xb0c4deff], - ['lightyellow', 0xffffe0ff], - ['lime', 0x00ff00ff], - ['limegreen', 0x32cd32ff], - ['linen', 0xfaf0e6ff], - ['magenta', 0xff00ffff], - ['maroon', 0x800000ff], - ['mediumaquamarine', 0x66cdaaff], - ['mediumblue', 0x0000cdff], - ['mediumorchid', 0xba55d3ff], - ['mediumpurple', 0x9370dbff], - ['mediumseagreen', 0x3cb371ff], - ['mediumslateblue', 0x7b68eeff], - ['mediumspringgreen', 0x00fa9aff], - ['mediumturquoise', 0x48d1ccff], - ['mediumvioletred', 0xc71585ff], - ['midnightblue', 0x191970ff], - ['mintcream', 0xf5fffaff], - ['mistyrose', 0xffe4e1ff], - ['moccasin', 0xffe4b5ff], - ['navajowhite', 0xffdeadff], - ['navy', 0x000080ff], - ['oldlace', 0xfdf5e6ff], - ['olive', 0x808000ff], - ['olivedrab', 0x6b8e23ff], - ['orange', 0xffa500ff], - ['orangered', 0xff4500ff], - ['orchid', 0xda70d6ff], - ['palegoldenrod', 0xeee8aaff], - ['palegreen', 0x98fb98ff], - ['paleturquoise', 0xafeeeeff], - ['palevioletred', 0xdb7093ff], - ['papayawhip', 0xffefd5ff], - ['peachpuff', 0xffdab9ff], - ['peru', 0xcd853fff], - ['pink', 0xffc0cbff], - ['plum', 0xdda0ddff], - ['powderblue', 0xb0e0e6ff], - ['purple', 0x800080ff], - ['rebeccapurple', 0x663399ff], - ['red', 0xff0000ff], - ['rosybrown', 0xbc8f8fff], - ['royalblue', 0x4169e1ff], - ['saddlebrown', 0x8b4513ff], - ['salmon', 0xfa8072ff], - ['sandybrown', 0xf4a460ff], - ['seagreen', 0x2e8b57ff], - ['seashell', 0xfff5eeff], - ['sienna', 0xa0522dff], - ['silver', 0xc0c0c0ff], - ['skyblue', 0x87ceebff], - ['slateblue', 0x6a5acdff], - ['slategray', 0x708090ff], - ['slategrey', 0x708090ff], - ['snow', 0xfffafaff], - ['springgreen', 0x00ff7fff], - ['steelblue', 0x4682b4ff], - ['tan', 0xd2b48cff], - ['teal', 0x008080ff], - ['thistle', 0xd8bfd8ff], - ['tomato', 0xff6347ff], - ['transparent', 0x00000000], - ['turquoise', 0x40e0d0ff], - ['violet', 0xee82eeff], - ['wheat', 0xf5deb3ff], - ['white', 0xffffffff], - ['whitesmoke', 0xf5f5f5ff], - ['yellow', 0xffff00ff], - ['yellowgreen', 0x9acd32ff], -]); \ No newline at end of file diff --git a/v2/ops/crop.mjs b/v2/ops/crop.mjs deleted file mode 100644 index 261f529..0000000 --- a/v2/ops/crop.mjs +++ /dev/null @@ -1,64 +0,0 @@ -function clamp(min, int, max) { - const t = int < min ? min : int; return t > max ? max : t; -} - -export function cut(x, y, width, height, framebuffer) { - width |= 0; - height |= 0; - const frame = new framebuffer.constructor(width, height); - - const n32 = frame.u32; - const o32 = framebuffer.u32; - const fwidth = framebuffer.width | 0; - - for (let yy = 0 | 0; yy < height; yy++) { - const offset = x + fwidth * (y + yy); - n32.set(o32.subarray(offset, width + offset), yy * width); - } - - return frame; -} - -export function crop(x, y, width, height, framebuffer) { - width |= 0; - height |= 0; - const old = framebuffer.u32; - const fwidth = framebuffer.width | 0; - const u32 = framebuffer.u32 = new Uint32Array(width * height); - - for (let yy = 0; yy < height; yy++) { - const offset = x + fwidth * (y + yy); - u32.set(old.subarray(offset, width + offset), yy * width); - } - - framebuffer.width = width; - framebuffer.height = height; - framebuffer.u8 = new Uint8Array(framebuffer.u32.buffer); - framebuffer.view = new DataView(framebuffer.u32.buffer); -} - -export function circle(feathering, framebuffer) { - const u8 = framebuffer.u8; - const u32 = framebuffer.u32; - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - const rad = Math.min(width, height) / 2; - - const cx = width / 2; - const cy = height / 2; - const rad_2 = rad ** 2; - const feathering_12 = feathering ** (1 / 2); - - for (let y = 0 | 0; y < height; y++) { - const cdy = (y - cy) ** 2; - const y_offset = y * width; - - for (let x = 0 | 0; x < width; x++) { - const cd = cdy + (x - cx) ** 2; - if (cd > rad_2) u32[x + y_offset] = 0; - else if (feathering) u8[3 + 4 * (x + y_offset)] *= clamp(0, 1 - (cd / rad_2) * feathering_12, 1); - } - } - - return framebuffer; -} \ No newline at end of file diff --git a/v2/ops/fill.mjs b/v2/ops/fill.mjs deleted file mode 100644 index 0743c25..0000000 --- a/v2/ops/fill.mjs +++ /dev/null @@ -1,33 +0,0 @@ -export function color(int, framebuffer) { - framebuffer.view.setUint32(0, int); - framebuffer.u32.fill(framebuffer.u32[0]); -} - -export function fn(cb, framebuffer) { - let offset = 0 | 0; - const view = framebuffer.view; - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - for (let y = 1 | 0; y <= height; y++) { - for (let x = 1 | 0; x <= width; x++) { - view.setUint32(offset, cb(x, y), false); offset += 4; - } - } -} - -export function swap(old, int, framebuffer) { - { - const t = new Uint32Array(2); - const v = new DataView(t.buffer); - old = (v.setUint32(0, old), t[0]); - int = (v.setUint32(4, int), t[1]); - } - - const u32 = framebuffer.u32; - const l = framebuffer.u32.length | 0; - - for (let o = 0 | 0; o < l; o++) { - if (old === u32[o]) u32[o] = int; - } -} \ No newline at end of file diff --git a/v2/ops/flip.mjs b/v2/ops/flip.mjs deleted file mode 100644 index 6d0902c..0000000 --- a/v2/ops/flip.mjs +++ /dev/null @@ -1,33 +0,0 @@ -export function horizontal(framebuffer) { - let offset = 0 | 0; - const u32 = framebuffer.u32; - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - for (let y = 0 | 0; y < height; y++) { - u32.subarray(offset, offset += width).reverse(); - } -} - -export function vertical(framebuffer) { - const u32 = framebuffer.u32; - const width = framebuffer.width | 0; - const oheight = framebuffer.height | 0; - const height = (framebuffer.height / 2) | 0; - - for (let y = 0 | 0; y < height; y++) { - const yo = y * width | 0; - const wo1y = width * (oheight - 1 - y) | 0; - - for (let x = 0 | 0; x < width; x++) { - const offset = x + yo; - const offset2 = x + wo1y; - - const top = u32[offset]; - const bottom = u32[offset2]; - - u32[offset2] = top; - u32[offset] = bottom; - } - } -} \ No newline at end of file diff --git a/v2/ops/index.mjs b/v2/ops/index.mjs deleted file mode 100644 index dcd9bd6..0000000 --- a/v2/ops/index.mjs +++ /dev/null @@ -1,9 +0,0 @@ -export * as flip from './flip.mjs'; -export * as fill from './fill.mjs'; -export * as blur from './blur.mjs'; -export * as crop from './crop.mjs'; -export * as color from './color.mjs'; -export * as resize from './resize.mjs'; -export * as rotate from './rotate.mjs'; -export * as overlay from './overlay.mjs'; -export * as iterator from './iterator.mjs'; \ No newline at end of file diff --git a/v2/ops/iterator.mjs b/v2/ops/iterator.mjs deleted file mode 100644 index bd460aa..0000000 --- a/v2/ops/iterator.mjs +++ /dev/null @@ -1,34 +0,0 @@ -export function* cords(framebuffer) { - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - for (let y = 0 | 0; y < height; y++) { - for (let x = 0 | 0; x < width; x++) yield [x, y]; - } -} - -export function* rgba(framebuffer) { - let offset = 0 | 0; - const u8 = framebuffer.u8; - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - for (let y = 0 | 0; y < height; y++) { - for (let x = 0 | 0; x < width; x++) { - yield [x, y, u8.subarray(offset, offset += 4)]; - } - } -} - -export function* u32(framebuffer) { - let offset = 0 | 0; - const view = framebuffer.view; - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - for (let y = 0 | 0; y < height; y++) { - for (let x = 0 | 0; x < width; x++) { - yield [x, y, view.getUint32(offset, false)]; offset += 4; - } - } -} \ No newline at end of file diff --git a/v2/ops/overlay.mjs b/v2/ops/overlay.mjs deleted file mode 100644 index b0184b1..0000000 --- a/v2/ops/overlay.mjs +++ /dev/null @@ -1,63 +0,0 @@ -export function replace(bg, fg, x, y) { - const b32 = bg.u32; - const f32 = fg.u32; - const fw = fg.width | 0; - const bw = bg.width | 0; - const fh = fg.height | 0; - const bh = bg.height | 0; - const ox = (x > 0 ? 0 : -x) | 0; - const oy = (y > 0 ? 0 : -y) | 0; - const top = (y > 0 ? y : 0) | 0; - const left = (x > 0 ? x : 0) | 0; - const width = (Math.min(bw, x + fw) - left) | 0; - const height = (Math.min(bh, y + fh) - top) | 0; - - if (0 >= width || 0 >= height) return; - - for (let yy = 0 | 0; yy < height; yy++) { - const yyoffset = ox + fw * (yy + oy); - const yoffset = left + bw * (yy + top); - b32.subarray(yoffset, width + yoffset).set(f32.subarray(yyoffset, width + yyoffset)); - } -} - -export function blend(bg, fg, x, y) { - const b32 = bg.u32; - const f32 = fg.u32; - const fw = fg.width | 0; - const bw = bg.width | 0; - const fh = fg.height | 0; - const bh = bg.height | 0; - const ox = (x > 0 ? 0 : -x) | 0; - const oy = (y > 0 ? 0 : -y) | 0; - const top = (y > 0 ? y : 0) | 0; - const left = (x > 0 ? x : 0) | 0; - const width = (Math.min(bw, x + fw) - left) | 0; - const height = (Math.min(bh, y + fh) - top) | 0; - - if (0 >= width || 0 >= height) return; - - for (let yy = 0 | 0; yy < height; yy++) { - const yyoffset = ox + fw * (yy + oy); - const yoffset = left + bw * (yy + top); - - for (let xx = 0 | 0; xx < width; xx++) { - const F = f32[xx + yyoffset]; - - // todo: be? - const fa = F >> 24 & 0xff; - if (fa === 0x00) continue; - else if (fa === 0xff) b32[xx + yoffset] = F; - - else { - const alpha = 1 + fa; - const inv_alpha = 256 - fa; - const B = b32[xx + yoffset]; - const r = (alpha * (F & 0xff) + inv_alpha * (B & 0xff)) >> 8; - const g = (alpha * ((F >> 8) & 0xff) + inv_alpha * ((B >> 8) & 0xff)) >> 8; - const b = (alpha * ((F >> 16) & 0xff) + inv_alpha * ((B >> 16) & 0xff)) >> 8; - b32[xx + yoffset] = (Math.max(fa, (B >> 24) & 0xff) << 24) | ((b & 0xff) << 16) | ((g & 0xff) << 8) | r; - } - } - } -} \ No newline at end of file diff --git a/v2/ops/resize.mjs b/v2/ops/resize.mjs deleted file mode 100644 index a822037..0000000 --- a/v2/ops/resize.mjs +++ /dev/null @@ -1,166 +0,0 @@ -function lerp(a, b, t) { - return t * b + a * (1 - t); -} - -function clamp(min, int, max) { - const t = int < min ? min : int; return t > max ? max : t; -} - -function clamped(x, y, width, height) { - return 4 * (clamp(0, x, width - 1) + clamp(0, y, height - 1) * width); -} - -function hermite(A, B, C, D, t) { - const c = (C / 2) + (A / -2); - const b = A + (C * 2) - (D / 2) - (B * 2.5); - const a = (D / 2) + (A / -2) + (B * 1.5) - (C * 1.5); - - const t2 = t * t; - return B + (c * t) + (b * t2) + (a * t * t2); -} - -export function nearest(width, height, framebuffer) { - width = width | 0; - height = height | 0; - const old = framebuffer.u32; - const fwidth = framebuffer.width | 0; - const fheight = framebuffer.height | 0; - const u32 = framebuffer.u32 = new Uint32Array(width * height); - - const dw = 1 / width; - const dh = 1 / height; - const xw = dw * fwidth; - const yw = dh * fheight; - for (let y = 0 | 0; y < height; y++) { - const yoffset = y * width; - const yyoffset = fwidth * ((y * yw) | 0); - - for (let x = 0 | 0; x < width; x++) { - u32[x + yoffset] = old[yyoffset + ((x * xw) | 0)]; - } - } - - framebuffer.width = width; - framebuffer.height = height; - framebuffer.u8 = new Uint8Array(u32.buffer); - framebuffer.view = new DataView(u32.buffer); -} - -export function linear(width, height, framebuffer) { - width = width | 0; - height = height | 0; - const old = framebuffer.u8; - const old_width = framebuffer.width | 0; - const old_height = framebuffer.height | 0; - const u8 = new Uint8ClampedArray(4 * width * height); - - let offset = 0 | 0; - const width1 = 1 / (width - 1); - const height1 = 1 / (height - 1); - - for (let y = 0 | 0; y < height; y++) { - const yy = old_height * (y * height1) - .5; - - const yyi = yy | 0; - const ty = yy - yyi; - - for (let x = 0 | 0; x < width; x++) { - const xx = old_width * (x * width1) - .5; - - const xxi = xx | 0; - const tx = xx - xxi; - const s0 = clamped(xxi, yyi, old_width, old_height); - const s1 = clamped(1 + xxi, yyi, old_width, old_height); - const s2 = clamped(xxi, 1 + yyi, old_width, old_height); - const s3 = clamped(1 + xxi, 1 + yyi, old_width, old_height); - - u8[offset++] = lerp(lerp(old[s0], old[s2], tx), lerp(old[s1], old[s3], tx), ty); - u8[offset++] = lerp(lerp(old[1 + s0], old[1 + s2], tx), lerp(old[1 + s1], old[1 + s3], tx), ty); - u8[offset++] = lerp(lerp(old[2 + s0], old[2 + s2], tx), lerp(old[2 + s1], old[2 + s3], tx), ty); - u8[offset++] = lerp(lerp(old[3 + s0], old[3 + s2], tx), lerp(old[3 + s1], old[3 + s3], tx), ty); - } - } - - framebuffer.width = width; - framebuffer.height = height; - framebuffer.u8 = new Uint8Array(u8.buffer); - framebuffer.view = new DataView(u8.buffer); - framebuffer.u32 = new Uint32Array(u8.buffer); -} - -export function cubic(width, height, framebuffer) { - width = width | 0; - height = height | 0; - const old = framebuffer.u8; - const old_width = framebuffer.width | 0; - const old_height = framebuffer.height | 0; - const u8 = new Uint8ClampedArray(4 * width * height); - - let offset = 0 | 0; - const width1 = 1 / (width - 1); - const height1 = 1 / (height - 1); - - for (let y = 0 | 0; y < height; y++) { - const yy = old_height * (y * height1) - .5; - - const yyi = yy | 0; - const ty = yy - yyi; - - for (let x = 0 | 0; x < width; x++) { - const xx = old_width * (x * width1) - .5; - - const xxi = xx | 0; - const tx = xx - xxi; - const s0 = clamped(xxi - 1, yyi - 1, old_width, old_height); - const s1 = clamped(0 + xxi, yyi - 1, old_width, old_height); - const s2 = clamped(1 + xxi, yyi - 1, old_width, old_height); - const s3 = clamped(2 + xxi, yyi - 1, old_width, old_height); - - const s4 = clamped(xxi - 1, yyi, old_width, old_height); - const s5 = clamped(0 + xxi, yyi, old_width, old_height); - const s6 = clamped(1 + xxi, yyi, old_width, old_height); - const s7 = clamped(2 + xxi, yyi, old_width, old_height); - - const s8 = clamped(xxi - 1, 1 + yyi, old_width, old_height); - const s9 = clamped(0 + xxi, 1 + yyi, old_width, old_height); - const s10 = clamped(1 + xxi, 1 + yyi, old_width, old_height); - const s11 = clamped(2 + xxi, 1 + yyi, old_width, old_height); - - const s12 = clamped(xxi - 1, 2 + yyi, old_width, old_height); - const s13 = clamped(0 + xxi, 2 + yyi, old_width, old_height); - const s14 = clamped(1 + xxi, 2 + yyi, old_width, old_height); - const s15 = clamped(2 + xxi, 2 + yyi, old_width, old_height); - - const c0 = hermite(old[s0], old[s1], old[s2], old[s3], tx); - const c00 = hermite(old[1 + s0], old[1 + s1], old[1 + s2], old[1 + s3], tx); - const c000 = hermite(old[2 + s0], old[2 + s1], old[2 + s2], old[2 + s3], tx); - const c0000 = hermite(old[3 + s0], old[3 + s1], old[3 + s2], old[3 + s3], tx); - - const c1 = hermite(old[s4], old[s5], old[s6], old[s7], tx); - const c11 = hermite(old[1 + s4], old[1 + s5], old[1 + s6], old[1 + s7], tx); - const c111 = hermite(old[2 + s4], old[2 + s5], old[2 + s6], old[2 + s7], tx); - const c1111 = hermite(old[3 + s4], old[3 + s5], old[3 + s6], old[3 + s7], tx); - - const c2 = hermite(old[s8], old[s9], old[s10], old[s11], tx); - const c22 = hermite(old[1 + s8], old[1 + s9], old[1 + s10], old[1 + s11], tx); - const c222 = hermite(old[2 + s8], old[2 + s9], old[2 + s10], old[2 + s11], tx); - const c2222 = hermite(old[3 + s8], old[3 + s9], old[3 + s10], old[3 + s11], tx); - - const c3 = hermite(old[s12], old[s13], old[s14], old[s15], tx); - const c33 = hermite(old[1 + s12], old[1 + s13], old[1 + s14], old[1 + s15], tx); - const c333 = hermite(old[2 + s12], old[2 + s13], old[2 + s14], old[2 + s15], tx); - const c3333 = hermite(old[3 + s12], old[3 + s13], old[3 + s14], old[3 + s15], tx); - - u8[offset++] = hermite(c0, c1, c2, c3, ty); - u8[offset++] = hermite(c00, c11, c22, c33, ty); - u8[offset++] = hermite(c000, c111, c222, c333, ty); - u8[offset++] = hermite(c0000, c1111, c2222, c3333, ty); - } - } - - framebuffer.width = width; - framebuffer.height = height; - framebuffer.u8 = new Uint8Array(u8.buffer); - framebuffer.view = new DataView(u8.buffer); - framebuffer.u32 = new Uint32Array(u8.buffer); -} \ No newline at end of file diff --git a/v2/ops/rotate.mjs b/v2/ops/rotate.mjs deleted file mode 100644 index 627ad61..0000000 --- a/v2/ops/rotate.mjs +++ /dev/null @@ -1,117 +0,0 @@ -export function rotate180(framebuffer) { - framebuffer.u32.reverse(); -} - -export function rotate90(framebuffer) { - const u32 = framebuffer.u32; - const old = framebuffer.u32.slice(); - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - framebuffer.width = height; - framebuffer.height = width; - - for (let y = 0 | 0; y < height; y++) { - const yoffset = y * width; - const heighty1 = height - 1 - y; - - for (let x = 0 | 0; x < width; x++) { - u32[heighty1 + x * height] = old[x + yoffset]; - } - } -} - -export function rotate270(framebuffer) { - const u32 = framebuffer.u32; - const old = framebuffer.u32.slice(); - const width = framebuffer.width | 0; - const height = framebuffer.height | 0; - - framebuffer.width = height; - framebuffer.height = width; - - for (let y = 0 | 0; y < height; y++) { - const yoffset = y * width; - - for (let x = 0 | 0; x < width; x++) { - u32[y + height * (width - 1 - x)] = old[x + yoffset]; - } - } -} - -// broken? -export function rotate(deg, framebuffer, resize) { - const rad = Math.PI * ((360 - deg) / 180); - - const sin = Math.sin(rad); - const cos = Math.cos(rad); - - const width = (resize ? Math.abs(framebuffer.width * sin) + Math.abs(framebuffer.height * cos) : framebuffer.width) | 0; - const height = (resize ? Math.abs(framebuffer.width * cos) + Math.abs(framebuffer.height * sin) : framebuffer.height) | 0; - - const same_size = width === framebuffer.width && height === framebuffer.height; - - const inn = same_size ? framebuffer.clone() : framebuffer; - const out = { width, height, u8: same_size ? framebuffer.u8 : new Uint8Array(4 * width * height) }; - - const out_cx = width / 2 - .5; - const out_cy = height / 2 - .5; - const src_cx = framebuffer.width / 2 - .5; - const src_cy = framebuffer.height / 2 - .5; - - let h = 0; - do { - let w = 0; - const ysin = src_cx - sin * (h - out_cy); - const ycos = src_cy + cos * (h - out_cy); - - do { - interpolate(inn, out, w, h, ysin + cos * (w - out_cx), ycos + sin * (w - out_cx)); - } while (w++ < width); - } while (h++ < height); - - framebuffer.u8 = out.u8; - framebuffer.width = width; - framebuffer.height = height; - framebuffer.view = new DataView(out.u8.buffer, out.u8.byteOffset, out.u8.byteLength); - framebuffer.u32 = new Uint32Array(out.u8.buffer, out.u8.byteOffset, out.u8.byteLength / 4); -} - -function interpolate(inn, out, x0, y0, x1, y1) { - const x2 = ~~x1; - const y2 = ~~y1; - const xq = x1 - x2; - const yq = y1 - y2; - const offset = 4 * (x0 + y0 * out.width); - - const ref = { r: 0, g: 0, b: 0, a: 0 }; - pawn(x2, y2, (1 - xq) * (1 - yq), ref, inn); - - pawn(1 + x2, y2, xq * (1 - yq), ref, inn); - pawn(x2, 1 + y2, (1 - xq) * yq, ref, inn); - - pawn(1 + x2, 1 + y2, xq * yq, ref, inn); - - out.u8[3 + offset] = ref.a; - out.u8[offset] = ref.r / ref.a; - out.u8[1 + offset] = ref.g / ref.a; - out.u8[2 + offset] = ref.b / ref.a; -} - -function pawn(point0, point1, weight, ref, inn) { - if ( - point0 > 0 - && point1 > 0 - && point0 < inn.width - && point1 < inn.height - ) { - const offset = 4 * (point0 + point1 * inn.width); - - const wa = weight * inn.u8[3 + offset]; - - ref.a += wa; - ref.r += wa * inn.u8[offset]; - ref.g += wa * inn.u8[1 + offset]; - ref.b += wa * inn.u8[2 + offset]; - } -} \ No newline at end of file diff --git a/v2/util/mem.mjs b/v2/util/mem.mjs deleted file mode 100644 index ce6114e..0000000 --- a/v2/util/mem.mjs +++ /dev/null @@ -1,24 +0,0 @@ -export function view(buffer, shared = false) { - if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer); - if (shared && buffer instanceof SharedArrayBuffer) return new Uint8Array(buffer); - if (ArrayBuffer.isView(buffer)) return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); - - throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'"); -} - -export function from_parts(buffers, shared = false) { - let length = 0; - let offset = 0; - buffers.forEach(buffer => length += (null == buffer.byteLength ? buffer.length : buffer.byteLength)); - - const u8 = new Uint8Array(shared ? new SharedArrayBuffer(length) : length); - - buffers.forEach(buffer => { - const ref = Array.isArray(buffer) ? buffer : view(buffer, true); - - u8.set(ref, offset); - offset += ref.length; - }); - - return u8; -} \ No newline at end of file diff --git a/wasm/any/font.wasm b/wasm/any/font.wasm deleted file mode 100755 index d8f9097..0000000 Binary files a/wasm/any/font.wasm and /dev/null differ diff --git a/wasm/any/gif.wasm b/wasm/any/gif.wasm deleted file mode 100755 index 748e86b..0000000 Binary files a/wasm/any/gif.wasm and /dev/null differ diff --git a/wasm/any/jpeg.wasm b/wasm/any/jpeg.wasm deleted file mode 100644 index 68c8cbd..0000000 Binary files a/wasm/any/jpeg.wasm and /dev/null differ diff --git a/wasm/any/png.wasm b/wasm/any/png.wasm deleted file mode 100755 index b8ab267..0000000 Binary files a/wasm/any/png.wasm and /dev/null differ diff --git a/wasm/any/svg.wasm b/wasm/any/svg.wasm deleted file mode 100755 index 6deebfd..0000000 Binary files a/wasm/any/svg.wasm and /dev/null differ diff --git a/wasm/any/tiff.wasm b/wasm/any/tiff.wasm deleted file mode 100755 index 3c04a44..0000000 Binary files a/wasm/any/tiff.wasm and /dev/null differ diff --git a/wasm/node/font.js b/wasm/node/font.js deleted file mode 100644 index 7cccdb1..0000000 --- a/wasm/node/font.js +++ /dev/null @@ -1,142 +0,0 @@ -const wasm_name = 'font'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - let registry = null; - const wasm = new WebAssembly.Instance(mod).exports; - - class mem { - static length() { return wasm.wlen(); } - static alloc(size) { return wasm.walloc(size); } - static free(ptr, size) { return wasm.wfree(ptr, size); } - static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } - static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } - - static copy_and_free(ptr, size) { - let slice = mem.u8(ptr, size).slice(); - return (wasm.wfree(ptr, size), slice); - } - } - const encode_utf8 = 'Deno' in globalThis ? Deno.core.encode : (() => { - const encoder = new TextEncoder(); - return string => encoder.encode(string); - })(); - - const decode_utf8 = 'Deno' in globalThis ? Deno.core.decode : (() => { - const decoder = new TextDecoder(); - return buffer => decoder.decode(buffer); - })(); - - if ('FinalizationRegistry' in globalThis) { - registry = new FinalizationRegistry(([t, ptr]) => { - if (t === 0) wasm.font_free(ptr); - if (t === 1) wasm.layout_free(ptr); - }); - } - - class Font { - constructor(scale, buffer) { - this.scale = scale; - const ptr = mem.alloc(buffer.length); - mem.u8(ptr, buffer.length).set(buffer); - this.ptr = wasm.font_new(ptr, buffer.length, scale); - - if (!this.ptr) throw new Error('invalid font'); - if (registry) registry.register(this, [0, this.ptr], this); - } - - free() { - this.ptr = wasm.font_free(this.ptr); - if (registry) registry.unregister(this); - } - - has(char) { - return wasm.font_has(this.ptr, String.prototype.charCodeAt.call(char, 0)); - } - - metrics(char, scale = this.scale) { - const ptr = wasm.font_metrics(this.ptr, String.prototype.charCodeAt.call(char, 0), scale); - const metrics = JSON.parse(decode_utf8(mem.u8(wasm.font_metrics_buffer(ptr), mem.length()))); - - return (wasm.font_metrics_free(ptr), metrics); - } - - rasterize(char, scale = this.scale) { - const ptr = wasm.font_rasterize(this.ptr, String.prototype.charCodeAt.call(char, 0), scale); - - const glyph = { - buffer: mem.u8(wasm.font_rasterize_buffer(ptr), mem.length()).slice(), - metrics: JSON.parse(decode_utf8(mem.u8(wasm.font_rasterize_metrics(ptr), mem.length()))), - } - - return (wasm.font_rasterize_free(ptr), glyph); - } - } - - class Layout { - constructor() { - this.ptr = wasm.layout_new(); - if (registry) this.refs = []; - if (registry) registry.register(this, [1, this.ptr], this); - } - - clear() { - wasm.layout_clear(this.ptr); - if (registry) this.refs.length = 0; - } - - lines() { - return wasm.layout_lines(this.ptr); - } - - free() { - if (registry) this.refs.length = 0; - this.ptr = wasm.layout_free(this.ptr); - if (registry) registry.unregister(this); - } - - reset(options = {}) { - options = encode_utf8(JSON.stringify(options)); - - if (registry) this.refs.length = 0; - const ptr = mem.alloc(options.length); - mem.u8(ptr, options.length).set(options); - wasm.layout_reset(this.ptr, ptr, options.length); - } - - append(font, text, init) { - text = encode_utf8(text); - const options = init || {}; - if (registry) this.refs.push(font); - const ptr = mem.alloc(text.length); - mem.u8(ptr, text.length).set(text); - const has_color = ('r' in options) || ('g' in options) || ('b' in options); - wasm.layout_append(this.ptr, font.ptr, ptr, text.length, options.scale == null ? font.scale : options.scale, has_color, options.r, options.g, options.b); - } - - rasterize(r, g, b) { - const ptr = wasm.layout_rasterize(this.ptr, r, g, b); - - const framebuffer = { - width: wasm.layout_rasterize_width(ptr), - height: wasm.layout_rasterize_height(ptr), - buffer: mem.u8(wasm.layout_rasterize_buffer(ptr), mem.length()).slice(), - } - - return (wasm.layout_rasterize_free(ptr), framebuffer); - } - } - - return { Font, Layout }; - } -} \ No newline at end of file diff --git a/wasm/node/gif.js b/wasm/node/gif.js deleted file mode 100644 index b2e8b15..0000000 --- a/wasm/node/gif.js +++ /dev/null @@ -1,76 +0,0 @@ -const wasm_name = 'gif'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - const wasm = new WebAssembly.Instance(mod, { env: { push_to_stream(id, ptr) { } } }).exports; - - class mem { - static length() { return wasm.wlen(); } - static alloc(size) { return wasm.walloc(size); } - static free(ptr, size) { return wasm.wfree(ptr, size); } - static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } - static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } - - static copy_and_free(ptr, size) { - let slice = mem.u8(ptr, size).slice(); - return (wasm.wfree(ptr, size), slice); - } - } - - function load(buffer) { - return [...new Decoder(buffer).frames()]; - } - - class Decoder { - constructor(buffer, limit = 0) { - const bptr = mem.alloc(buffer.length); - mem.u8(bptr, buffer.length).set(buffer); - this.ptr = wasm.decoder_new(bptr, buffer.length, limit); - if (0 === this.ptr) throw new Error('gif: failed to parse gif header'); - - this.width = wasm.decoder_width(this.ptr); - this.height = wasm.decoder_height(this.ptr); - } - - free() { - this.ptr = wasm.decoder_free(this.ptr); - } - - *frames() { - let frame; - while (frame = this.frame()) yield frame; - } - - frame() { - const ptr = wasm.decoder_frame(this.ptr); - - if (1 === ptr) return null; - if (0 === ptr) throw (this.free(), new Error('gif: failed to decode frame')); - - const framebuffer = { - x: wasm.decoder_frame_x(ptr), - y: wasm.decoder_frame_y(ptr), - delay: wasm.decoder_frame_delay(ptr), - width: wasm.decoder_frame_width(ptr), - height: wasm.decoder_frame_height(ptr), - dispose: wasm.decoder_frame_dispose(ptr), - buffer: mem.u8(wasm.decoder_frame_buffer(ptr), mem.length()).slice(), - }; - - return (wasm.decoder_frame_free(ptr), framebuffer); - } - } - - return { load, Decoder }; - } -} diff --git a/wasm/node/jpeg.js b/wasm/node/jpeg.js deleted file mode 100644 index a611404..0000000 --- a/wasm/node/jpeg.js +++ /dev/null @@ -1,92 +0,0 @@ -const wasm_name = 'jpeg'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - const wasm = new WebAssembly.Instance(mod).exports; - - class mem { - static length() { return wasm.wlen(); } - static alloc(size) { return wasm.walloc(size); } - static free(ptr, size) { return wasm.wfree(ptr, size); } - static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } - static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } - - static copy_and_free(ptr, size) { - let slice = mem.u8(ptr, size).slice(); - return (wasm.wfree(ptr, size), slice); - } - } - - function decode(buffer, width, height) { - const bptr = mem.alloc(buffer.length); - mem.u8(bptr, buffer.length).set(buffer); - const ptr = wasm.decode(bptr, buffer.length, width, height); - - if (0 === ptr) throw new Error('jpg: failed to decode'); - if (1 === ptr) throw new Error('jpg: failed to scale decoder'); - - const framebuffer = { - width: wasm.decode_width(ptr), - height: wasm.decode_height(ptr), - format: wasm.decode_format(ptr), - buffer: mem.u8(wasm.decode_buffer(ptr), mem.length()).slice(), - } - - return (wasm.decode_free(ptr), framebuffer); - } - - function load(buffer) { - const framebuffer = decode(buffer); - - const old = framebuffer.buffer; - if (2 !== framebuffer.format) framebuffer.buffer = new Uint8Array(4 * framebuffer.width * framebuffer.height); - - if (0 === framebuffer.format) { - let offset = 0; - const view = new DataView(framebuffer.buffer.buffer); - while (offset < old.length) view.setUint32(4 * offset, old[offset] << 24 | old[offset] << 16 | old[offset++] << 8 | 0xff, false); - } - - else if (2 === framebuffer.format) { - let offset = 0; - while (offset < old.length) { - const k = old[3 + offset]; - old[offset] = k * old[offset++] / 255; - old[offset] = k * old[offset++] / 255; - old[offset] = k * old[offset++] / 255; - - old[offset++] = 0xff; - } - } - - else if (1 === framebuffer.format) { - let offset = 0; - let foffset = 0; - framebuffer.buffer.fill(0xff); - const length = 3 * framebuffer.width * framebuffer.height; - - while (offset < length) { - framebuffer.buffer[foffset++] = old[offset++]; - framebuffer.buffer[foffset++] = old[offset++]; - framebuffer.buffer[foffset++] = old[offset++]; - - foffset++; - } - } - - return framebuffer; - } - - return { load, decode }; - } -} \ No newline at end of file diff --git a/wasm/node/png.js b/wasm/node/png.js deleted file mode 100644 index 911b1a3..0000000 --- a/wasm/node/png.js +++ /dev/null @@ -1,49 +0,0 @@ -const wasm_name = 'png'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - let u8; - - const { - wfree, walloc, decode, memory, - width: wwidth, height: wheight, - } = new WebAssembly.Instance(mod, { - env: { - emscripten_notify_memory_growth() { - u8 = new Uint8Array(memory.buffer); - }, - }, - }).exports; - - u8 = new Uint8Array(memory.buffer); - - return { - decode(buffer) { - const ptr = walloc(buffer.length); - - u8.set(buffer, ptr); - const status = decode(ptr, buffer.length); - - wfree(ptr); - if (0 > status) throw new Error(`png: failed to decode (${status})`); - - const width = wwidth(); - const height = wheight(); - const framebuffer = u8.slice(status, status + 4 * width * height); - - wfree(status); - return { width, height, framebuffer }; - }, - }; - } -} diff --git a/wasm/node/svg.js b/wasm/node/svg.js deleted file mode 100644 index 63478d9..0000000 --- a/wasm/node/svg.js +++ /dev/null @@ -1,56 +0,0 @@ -const wasm_name = 'svg'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - const wasm = new WebAssembly.Instance(mod).exports; - - class mem { - static length() { return wasm.wlen(); } - static alloc(size) { return wasm.walloc(size); } - static free(ptr, size) { return wasm.wfree(ptr, size); } - static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } - static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } - - static copy_and_free(ptr, size) { - let slice = mem.u8(ptr, size).slice(); - return (wasm.wfree(ptr, size), slice); - } - } - - function load(buffer, fit) { - if (!fit) return rasterize(buffer, 0, 0); - if (fit.zoom) return rasterize(buffer, 1, fit.zoom); - if (fit.width) return rasterize(buffer, 2, fit.width); - if (fit.height) return rasterize(buffer, 3, fit.height); - } - - function rasterize(buffer, fit, scale) { - const bptr = mem.alloc(buffer.length); - mem.u8(bptr, buffer.length).set(buffer); - const ptr = wasm.rasterize(bptr, buffer.length, fit, scale); - - if (0 === ptr) throw new Error('svg: failed to parse'); - if (1 === ptr) throw new Error('svg: failed to rasterize'); - - const framebuffer = { - width: wasm.rasterize_width(ptr), - height: wasm.rasterize_height(ptr), - buffer: mem.u8(wasm.rasterize_buffer(ptr), mem.length()).slice(), - } - - return (wasm.rasterize_free(ptr), framebuffer); - } - - return { load, rasterize }; - } -} \ No newline at end of file diff --git a/wasm/node/tiff.js b/wasm/node/tiff.js deleted file mode 100644 index 3c18524..0000000 --- a/wasm/node/tiff.js +++ /dev/null @@ -1,47 +0,0 @@ -const wasm_name = 'tiff'; -const { join } = require('path'); -const { promises: { readFile } } = require('fs'); -const wasm_path = process.env.IMAGESCRIPT_WASM_SIMD ? 'simd' : 'any'; - -let mod = null; -module.exports = { - async init() { - if (!mod) mod = new WebAssembly.Module(await readFile(join(__dirname, `../${wasm_path}/${wasm_name}.wasm`))); - - return this.new(); - }, - - new() { - const wasm = new WebAssembly.Instance(mod).exports; - - class mem { - static length() { return wasm.wlen(); } - static alloc(size) { return wasm.walloc(size); } - static free(ptr, size) { return wasm.wfree(ptr, size); } - static u8(ptr, size) { return new Uint8Array(wasm.memory.buffer, ptr, size); } - static u32(ptr, size) { return new Uint32Array(wasm.memory.buffer, ptr, size); } - - static copy_and_free(ptr, size) { - let slice = mem.u8(ptr, size).slice(); - return (wasm.wfree(ptr, size), slice); - } - } - - function decode(buffer) { - const bptr = mem.alloc(buffer.length); - mem.u8(bptr, buffer.length).set(buffer); - const ptr = wasm.decode(bptr, buffer.length); - if (0 === ptr) throw new Error('tiff: failed to decode'); - - const framebuffer = { - width: wasm.decode_width(ptr), - height: wasm.decode_height(ptr), - buffer: mem.u8(wasm.decode_buffer(ptr), mem.length()).slice(), - } - - return (wasm.decode_free(ptr), framebuffer); - } - - return { decode, load: decode }; - } -} \ No newline at end of file