diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f2c16c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: matmen, evanwashere + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform:** + - OS: [e.g. iOS, Windows] + - Environment [e.g. Chrome, Firefox, NodeJS, Deno] + - Version [e.g. 1.0.1, 1.1.20] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7cbe67c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FR]" +labels: enhancement +assignees: matmen + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codacy-analysis.yml b/.github/workflows/codacy-analysis.yml new file mode 100644 index 0000000..94d1293 --- /dev/null +++ b/.github/workflows/codacy-analysis.yml @@ -0,0 +1,49 @@ +# This workflow checks out code, performs a Codacy security scan +# and integrates the results with the +# GitHub Advanced Security code scanning feature. For more information on +# the Codacy security scan action usage and parameters, see +# https://github.com/codacy/codacy-analysis-cli-action. +# For more information on Codacy Analysis CLI in general, see +# https://github.com/codacy/codacy-analysis-cli. + +name: Codacy Security Scan + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '0 12 * * *' + +jobs: + codacy-security-scan: + name: Codacy Security Scan + runs-on: ubuntu-latest + steps: + # Checkout the repository to the GitHub Actions runner + - name: Checkout code + uses: actions/checkout@v2 + + # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis + - name: Run Codacy Analysis CLI + uses: codacy/codacy-analysis-cli-action@1.1.0 + with: + # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository + # You can also omit the token and run the tools that support default configurations + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + verbose: true + output: results.sarif + format: sarif + # Adjust severity of non-security issues + gh-code-scanning-compat: true + # Force 0 exit code to allow SARIF file generation + # This will handover control about PR rejection to the GitHub side + max-allowed-issues: 2147483647 + + # Upload the SARIF file generated in the previous step + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: results.sarif diff --git a/.github/workflows/node.js.yml b/.github/workflows/deno.yml similarity index 51% rename from .github/workflows/node.js.yml rename to .github/workflows/deno.yml index c26282d..0f2efb6 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/deno.yml @@ -1,13 +1,13 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Node.js CI +name: Deno CI on: push: - branches: [ master, dev ] + branches: [deno] pull_request: - branches: [ master, dev ] + branches: [deno] jobs: build: @@ -16,13 +16,12 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 15.x] + deno: ["v2.x", "canary"] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm run build --if-present - - run: npm test + - uses: actions/checkout@v4 + - name: Setup Deno ${{ matrix.deno }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ matrix.deno }} + - run: deno run --allow-read --allow-run tests/run.js diff --git a/.gitignore b/.gitignore index 4f4dcc2..d229b68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +old/ .idea/ -.nyc_output/ coverage/ -.node_modules/ \ No newline at end of file +.DS_Store +.nyc_output/ +node_modules/ \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 3c66c55..0000000 --- a/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -.idea/ -.github/ -tests/ -.nyc_output/ -coverage/ \ No newline at end of file diff --git a/.npmrc b/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/ImageScript.d.ts b/ImageScript.d.ts new file mode 100644 index 0000000..0c9ae4c --- /dev/null +++ b/ImageScript.d.ts @@ -0,0 +1,839 @@ +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; + + /** + * 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 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 index 63d9106..05cec48 100644 --- a/ImageScript.js +++ b/ImageScript.js @@ -1,13 +1,26 @@ -const png = require('./utils/png'); -const gif = require('./utils/gif'); -const fontlib = require('./utils/wasm/font'); -const svglib = require('./utils/wasm/svg'); -const jpeglib = require('./utils/wasm/jpeg'); +import v2 from './v2/framebuffer.mjs'; +import * as png from './utils/png.js'; +import * as svglib from './utils/wasm/svg.js'; +import * as giflib from './utils/wasm/gif.js'; +import * as pnglib from './utils/wasm/png.js'; +import * as fontlib from './utils/wasm/font.js'; +import * as jpeglib from './utils/wasm/jpeg.js'; +import * as tifflib from './utils/wasm/tiff.js'; + +const decode_utf8 = globalThis.Deno?.core?.decode ?? TextDecoder.prototype.decode.bind(new TextDecoder); +const encode_utf8 = globalThis.Deno?.core?.encode ?? globalThis.Buffer?.from.bind(globalThis.Buffer) ?? TextEncoder.prototype.encode.bind(new TextEncoder); + +const MAGIC_NUMBERS = { + PNG: 0x89504e47, + JPEG: 0xffd8ff, + TIFF: 0x49492a00, + GIF: 0x474946 +}; /** * Represents an image; provides utility functions */ -class Image { +export class Image { /** * Creates a new image with the given dimensions * @param {number} width @@ -48,11 +61,6 @@ class Image { return `Image<${this.width}x${this.height}>`; } - /** @private */ - static new(width, height) { - return new this(width, height); - } - /** * The images width * @returns {number} @@ -85,7 +93,6 @@ class Image { /** * Yields an [x,y,color] array for every pixel in the image * @yields {[number, number, number]} The coordinates and color of the pixel - * @returns {void} */ * iterateWithColors() { let offset = 0; @@ -235,7 +242,7 @@ class Image { */ getPixelAt(x, y) { this.__check_boundaries__(x, y); - return this.__view__.getUint32((~~y - 1) * this.width + (~~x - 1), false); + return this.__view__.getUint32(((~~y - 1) * this.width + (~~x - 1)) * 4, false); } /** @@ -312,19 +319,7 @@ class Image { * @returns {Image} */ fill(color) { - const type = typeof color; - if (type !== 'function') { - this.__view__.setUint32(0, color, false); - this.__u32__.fill(this.__u32__[0]); - } else { - let offset = 0; - for (let y = 1; y <= this.height; y++) { - for (let x = 1; x <= this.width; x++) { - this.__view__.setUint32(offset, color(x, y), false); - offset += 4; - } - } - } + new v2(this.width, this.height, this.bitmap).fill(color); return this; } @@ -334,7 +329,7 @@ class Image { * @returns {Image} */ clone() { - const image = Image.new(this.width, this.height); + const image = new Image(this.width, this.height); image.bitmap.set(this.bitmap); return image; } @@ -375,6 +370,54 @@ class Image { * @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; @@ -398,22 +441,11 @@ class Image { */ __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); - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const ySrc = Math.floor((y * this.height) / height); - const xSrc = Math.floor((x * this.width) / width); - - const destPos = (y * width + x) * 4; - const srcPos = (ySrc * this.width + xSrc) * 4; + image.bitmap.set(frame.u8); - image.__view__.setUint32(destPos, this.__view__.getUint32(srcPos, false), false); - } - } - - this.__apply__(image); - - return this; + return image; } /** @@ -428,7 +460,7 @@ class Image { if (width > this.width) width = this.width; if (height > this.height) height = this.height; - return this.__apply__(this.__crop__(x, y, width, height)); + return this.__apply__(this.__crop__(~~x, ~~y, ~~width, ~~height)); } /** @@ -463,8 +495,10 @@ class Image { * @returns {Image} */ drawBox(x, y, width, height, color) { - x -= 1; - y -= 1; + x = ~~(x - 1); + y = ~~(y - 1); + width = ~~width; + height = ~~height; if (typeof color === 'function') { for (let tY = 1; tY <= height; tY++) { @@ -543,19 +577,7 @@ class Image { * @returns {Image} */ cropCircle(max = false, feathering = 0) { - const rad = Math[max ? 'max' : 'min'](this.width, this.height) / 2; - const radSquared = rad ** 2; - const centerX = this.width / 2; - const centerY = this.height / 2; - - for (const [x, y] of this) { - const distanceFromCenter = (x - centerX) ** 2 + (y - centerY) ** 2; - const alphaIdx = ((y - 1) * this.width + (x - 1)) * 4 + 3; - if (distanceFromCenter > radSquared) - this.bitmap[alphaIdx] = 0; - else if (feathering) - this.bitmap[alphaIdx] *= Math.max(0, Math.min(1, 1 - (distanceFromCenter / radSquared) * feathering ** (1 / 2))); - } + new v2(this.width, this.height, this.bitmap).crop('circle', feathering); return this; } @@ -671,48 +693,11 @@ class Image { * @returns {Image} */ composite(source, x = 0, y = 0) { - x = ~~x; - y = ~~y; - - for (let yy = 0; yy < source.height; yy++) { - let y_offset = y + yy; - if (y_offset < 0) continue; - if (y_offset >= this.height) break; - - for (let xx = 0; xx < source.width; xx++) { - let x_offset = x + xx; - if (x_offset < 0) continue; - if (x_offset >= this.width) break; - - const offset = 4 * (x_offset + y_offset * this.width); - const fg = source.__view__.getUint32(4 * (xx + yy * source.width), false); - const bg = this.__view__.getUint32(offset, false); - - if ((fg & 0xff) === 0xff) this.__view__.setUint32(offset, fg, false); - else if ((fg & 0xff) === 0x00) this.__view__.setUint32(offset, bg, false); - else this.__view__.setUint32(offset, Image.__alpha_blend__(fg, bg), false); - } - } + new v2(this.width, this.height, this.bitmap).overlay(new v2(source.width, source.height, source.bitmap), x, y); return this; } - /** - * @private - * @param {number} fg - * @param {number} bg - * @returns {number} - */ - static __alpha_blend__(fg, bg) { - const fa = fg & 0xff; - const alpha = fa + 1; - const inv_alpha = 256 - fa; - 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)); - } - /** * Inverts the images colors * @returns {Image} @@ -835,114 +820,29 @@ class Image { * @param {boolean} resize Whether to resize the image so it fits all pixels or just ignore outlying pixels */ rotate(angle, resize = true) { - if (angle % 360 === 0) return this; - if (angle % 180 === 0) return this.__rotate_180__(); - - const rad = Math.PI * (angle / 180); - - const sin = Math.sin(rad); - const cos = Math.cos(rad); - - const width = resize - ? Math.abs(this.width * sin) + Math.abs(this.height * cos) - : this.width; - const height = resize - ? Math.abs(this.width * cos) + Math.abs(this.height * sin) - : this.height; - - const out = Image.new(width, height); + const frame = new v2(this.width, this.height, this.bitmap).rotate(360 - (angle % 360), resize); - const out_cx = width / 2 - .5; - const out_cy = height / 2 - .5; - const src_cx = this.width / 2 - .5; - const src_cy = this.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 { - const xf = ysin + cos * (w - out_cx); - const yf = ycos + sin * (w - out_cx); - Image.__interpolate__(this, out, w, h, xf, yf); - } while (w++ < width); - } while (h++ < height); + const out = new Image(frame.width, frame.height); + out.bitmap.set(frame.u8); return this.__apply__(out); } /** - * @returns {Image} - * @private + * Flips / mirrors the image horizontally or vertically + * @param {'horizontal' | 'vertical'} direction The direction to flip */ - __rotate_180__() { - let offset = 0; - this.bitmap.reverse(); - while (offset < this.bitmap.length) this.bitmap.subarray(offset, offset += 4).reverse(); + flip(direction) { + const frame = new v2(this.width, this.height, this.bitmap).flip(direction); + this.bitmap.set(frame.u8); return this; } /** - * @param {Image} src - * @param {Image} out - * @param {number} x0 - * @param {number} y0 - * @param {number} x1 - * @param {number} y1 * @private - */ - static __interpolate__(src, out, x0, y0, x1, y1) { - const x2 = ~~x1; - const y2 = ~~y1; - const xq = x1 - x2; - const yq = y1 - y2; - const out_slice = out.bitmap.subarray(4 * (x0 + y0 * out.width), -4); - - const ref = { - r: 0, - g: 0, - b: 0, - a: 0, - }; - - Image.__pawn__(x2, y2, (1 - xq) * (1 - yq), ref, src); - Image.__pawn__(1 + x2, y2, xq * (1 - yq), ref, src); - Image.__pawn__(x2, 1 + y2, (1 - xq) * yq, ref, src); - Image.__pawn__(1 + x2, 1 + y2, xq * yq, ref, src); - - out_slice[3] = ref.a; - out_slice[0] = ref.r / ref.a; - out_slice[1] = ref.g / ref.a; - out_slice[2] = ref.b / ref.a; - } - - /** @private */ - static __pawn__(point0, point1, weight, ref, src) { - if ( - point0 > 0 - && point1 > 0 - && point0 < src.width - && point1 < src.height - ) { - const offset = 4 * (point0 + point1 * src.width); - const src_slice = src.bitmap.subarray(offset, offset + 4); - - const wa = weight * src_slice[3]; - - ref.a += wa; - ref.r += wa * src_slice[0]; - ref.g += wa * src_slice[1]; - ref.b += wa * src_slice[2]; - } - } - - /** - * @private - * @param {Image} image - * @returns {Image} + * @param {Image|Frame} image + * @returns {Image|Frame} */ __apply__(image) { this.__width__ = image.__width__; @@ -951,6 +851,9 @@ class Image { 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; } @@ -1070,12 +973,43 @@ class Image { }; } + 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); + } + /** * Encodes the image into a PNG * @param {number} compression The compression level to use (0-3) * @return {Promise} The encoded data */ async encode(compression = 1) { + // return new v2(this.width, this.height, this.bitmap).encode('png', { compression: 'fast' }); return await png.encode(this.bitmap, {width: this.width, height: this.height, level: compression, channels: 4}); } @@ -1085,15 +1019,11 @@ class Image { * @return {Promise} */ async encodeJPEG(quality = 90) { - quality = Math.max(1, Math.min(100, quality)); - const jpegCanvas = new this.constructor(this.width, this.height); - jpegCanvas.fill(0xff); - jpegCanvas.composite(this); - return jpeglib.encode(this.width, this.height, quality, jpegCanvas.bitmap); + return jpeglib.encode(this.bitmap, this.width, this.height, Math.max(1, Math.min(100, quality))); } /** - * Decodes an image (PNG or JPEG) + * Decodes an image (PNG, JPEG or TIFF) * @param {Buffer|Uint8Array} data The binary data to decode * @return {Promise} The decoded image */ @@ -1109,17 +1039,19 @@ class Image { view = new DataView(data.buffer, data.byteOffset, data.byteLength); } - if (view.getUint32(0, false) === 0x89504e47) { // PNG - const {width, height, pixels} = await png.decode(data); - image = new this(width, height); + if (ImageType.isPNG(view)) { // PNG + const {width, height, framebuffer: pixels} = pnglib.decode(data); + image = new Image(width, height); image.bitmap.set(pixels); - } else if ((view.getUint32(0, false) >>> 8) === 0xffd8ff) { // JPEG - const status = await jpeglib.decode(0, data, 0, 0); - if (status === 1) throw new Error('Failed decoding JPEG image'); - const [pixelType, width, height] = jpeglib.meta(0); - image = new this(width, height); - const buffer = jpeglib.buffer(0); - jpeglib.free(0); + } else if (ImageType.isJPEG(view)) { // JPEG + const framebuffer = jpeglib.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); @@ -1140,6 +1072,15 @@ class Image { image.bitmap[i + 3] = 0xff; } } + } else if (ImageType.isTIFF(view)) { // TIFF + const status = await tifflib.decode(0, data); + if (status === 1) throw new Error('Failed decoding TIFF image'); + const meta = tifflib.meta(0); + const buffer = tifflib.buffer(0); + tifflib.free(0); + + image = new this(...meta); + image.bitmap.set(buffer); } else throw new Error('Unsupported image type'); return image; @@ -1174,9 +1115,9 @@ class Image { * @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 + * @return {Image} The rendered SVG graphic */ - static async renderSVG(svg, size = 1, mode = this.SVG_MODE_SCALE) { + static 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'); @@ -1185,33 +1126,14 @@ class Image { if (mode !== this.SVG_MODE_SCALE && size < 1) throw new RangeError('SVG size must be >= 1') - if (typeof svg !== 'string') - svg = svg.toString(); + if (typeof svg === 'string') svg = encode_utf8(svg); - const status = await svglib.rgba(0, svg, mode, size, size, size); - if (status === 1) throw new Error('Failed parsing SVG'); - if (status === 2) throw new Error('Failed rendering SVG'); - const meta = svglib.meta(0); - const image = new this(...meta); - image.bitmap.set(svglib.buffer(0)); - svglib.free(0); - return image; - } + const framebuffer = svglib.rasterize(svg, mode, size); + const image = new Image(framebuffer.width, framebuffer.height); - /** - * Wrap at individual characters. For use with {@link Image.renderText} - * @return {boolean} - */ - static get WRAP_STYLE_CHAR() { - return true; - } + image.bitmap.set(framebuffer.buffer); - /** - * Wrap at word ends. For use with {@link Image.renderText} - * @return {boolean} - */ - static get WRAP_STYLE_WORD() { - return false; + return image; } /** @@ -1220,22 +1142,35 @@ class Image { * @param {number} scale Font size to use * @param {string} text Text to render * @param {number} color Text color to use - * @param {number} wrapWidth Image width before wrapping - * @param {boolean} wrapStyle Whether to break at words ({@link WRAP_STYLE_WORD}) or at characters ({@link WRAP_STYLE_CHAR}) - * @return {Promise} The rendered text + * @param {TextLayout} layout The text layout to use + * @return {Image} The rendered text */ - static async renderText(font, scale, text, color = 0xffffffff, wrapWidth = Infinity, wrapStyle = this.WRAP_STYLE_WORD) { + static renderText(font, scale, text, color = 0xffffffff, layout = new TextLayout()) { + font = new fontlib.Font(scale, font); const [r, g, b, a] = Image.colorToRGBA(color); - await fontlib.load(0, font, scale); - fontlib.render(0, 0, scale, r, g, b, text, wrapWidth === Infinity ? null : wrapWidth, wrapStyle); - const buffer = fontlib.buffer(0); - const [width, height] = fontlib.meta(0); - fontlib.free(0); - const image = new this(width, height); - image.bitmap.set(buffer); - image.opacity(a / 0xff); - return image; + const layoutOptions = new fontlib.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); } } @@ -1244,20 +1179,50 @@ class Image { * Represents a frame in a GIF * @extends Image */ -class Frame extends Image { +export class Frame extends Image { + /** + * GIF frame disposal mode KEEP. For use with {@link Frame} + * @returns {number} + */ + static get DISPOSAL_KEEP() { + return 1; + } + + /** + * GIF frame disposal mode PREVIOUS. For use with {@link Frame} + * @returns {number} + */ + static get DISPOSAL_PREVIOUS() { + return 2; + } + + /** + * GIF frame disposal mode BACKGROUND. For use with {@link Frame} + * @returns {number} + */ + static get DISPOSAL_BACKGROUND() { + return 3; + } + /** * 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 {number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode * @return {Frame} */ - constructor(width, height, duration = 100) { + 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; } toString() { @@ -1268,92 +1233,342 @@ class Frame extends Image { * 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 {number} [disposalMode=Frame.DISPOSAL_KEEP] The frames disposal mode * @return {Frame} */ - static from(image, duration) { + 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); + 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 { +export class GIF extends Array { /** * Creates a new GIF image. * @param {Frame[]} frames The frames to create the GIF from - * @param {number} [loopCount=0] How often to loop the GIF for (0 = unlimited) + * @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 = 0) { - for (const frame of frames) { + constructor(frames, loopCount = -1) { + super(...frames); + + for (const frame of this) if (!(frame instanceof Frame)) - throw new TypeError(`Frame ${frames.indexOf(frame)} is not an instance of Frame`); - } + throw new TypeError(`Frame ${this.indexOf(frame)} is not an instance of Frame`); - if (loopCount < 0 || isNaN(loopCount)) + if (loopCount < -1 || isNaN(loopCount)) throw new RangeError('Invalid loop count'); - super(...frames); this.loopCount = loopCount; } + get width() { + let max = 0; + for (const frame of this) { + let width = frame.width + frame.xOffset; + if (max < width) + max = width; + } + + return max; + } + + 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>`; } + * [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); + return this.reduce((acc, frame) => acc + frame.duration, 0); } /** - * The GIFs width - * @return {number} + * Encodes the image into a GIF + * @param {number} [quality=10] GIF quality ((best) 1..30 (worst)) + * @return {Promise} The encoded data */ - get width() { - return Math.max(...[...this].map(frame => frame.width)); + async encode(quality = 10) { + const encoder = new giflib.Encoder(this.width, this.height, this.loopCount); + + for (const frame of this) { + if (!(frame instanceof Frame)) throw new Error('GIF contains invalid frames'); + encoder.add(frame.xOffset, frame.yOffset, ~~(frame.duration / 10), frame.width, frame.height, frame.bitmap, frame.disposalMode, quality); + } + + return encoder.u8(); } /** - * The GIFs height - * @return {number} + * 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 */ - get height() { - return Math.max(...[...this].map(frame => frame.height)); + static async decode(data, onlyExtractFirstFrame = false) { + let image; + + 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 ((view.getUint32(0, false) >>> 8) === 0x474946) { // GIF + const frames = []; + const decoder = new giflib.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; + } + + decoder.free(); + 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); + } +} + +export class TextLayout { /** - * Encodes the image into a GIF - * @return {Uint8Array} The encoded data + * Layout options for {@link 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='string'] 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 */ - encode() { - const frames = []; - for (const frame of this) { - frames.push({ - delay: frame.duration, - width: frame.width, - height: frame.height, - pixels: frame.bitmap - }); + 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 = wrapHardBreaks ?? true; + 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); } - return gif(frames, { - width: this.width, - height: this.height, - comment: 'powered by ImageScript', - loop: this.loopCount - }); + 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; } } -module.exports = {Image, GIF, Frame}; \ No newline at end of file +/** + * 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 + */ +export function decode(data, onlyExtractFirstFrame) { + const type = ImageType.getType(data); + + if (type === 'gif') + return GIF.decode(data, onlyExtractFirstFrame); + return Image.decode(data); +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d2197d --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +This software is licensed under the following license(s): +- GNU AFFERO GENERAL PUBLIC LICENSE, Version 3 +- MIT License + +== SPDX-License-Identifier: AGPL-3.0-or-later OR MIT == + +You may choose to comply with either one of the above +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 diff --git a/LICENSE.AGPL-3.0 b/LICENSE.AGPL-3.0 new file mode 100644 index 0000000..ce0100f --- /dev/null +++ b/LICENSE.AGPL-3.0 @@ -0,0 +1,619 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE.MIT b/LICENSE.MIT new file mode 100644 index 0000000..8f0286a --- /dev/null +++ b/LICENSE.MIT @@ -0,0 +1,19 @@ +Copyright (c) 2023 Mathis Mensing + +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/README.md b/README.md index b373ec1..a1d6687 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,53 @@ # ImageScript ##### zero-dependency JavaScript image manipulation -[![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/Documentationn-informational?style=for-the-badge)](https://oss-is.dreadful.tech/) +![NPM Version](https://img.shields.io/npm/v/imagescript?style=for-the-badge&label=NPM%20(Node.JS)) +![JSR Version](https://img.shields.io/jsr/v/%40matmen/imagescript?style=for-the-badge&label=JSR%20(Deno)) +[![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://img.shields.io/badge/deno.land-181717?logo=deno&style=for-the-badge)](https://deno.land/x/imagescript) -[![NPM](https://nodei.co/npm/imagescript.png)](https://www.npmjs.com/package/imagescript) +[![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) --- -**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 for compression and encoding. +**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. --- ### Features -- Loading PNGs (grayscale, RGB, indexed colors) with alpha support -- Loading JPEGs -- Rendering SVGs -- Rendering vector fonts -- Image manipulation functions (crop, rotate, composite, ...) -- Color manipulation functions (invert, shiftHue, ...) -- Encoding images as PNGs, JPEGs and GIFs + +- [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) + and [GIFs](https://imagescript.matmen.dev/GIF.html#encode) --- ### Example -[![Output](./tests/targets/readme.png)](./tests/readme.js) +```js +import {Image} from "jsr:@matmen/imagescript"; + +const input = await Deno.readFile('./image.png'); +const image = await Image.decode(input); +image.rotate(180); + +const output = await image.encode(); +await Deno.writeFile('./output.png', output); +``` --- -If you have any additional questions, feel free to join the [discord support server](https://discord.gg/8hPrwAH). \ No newline at end of file +If you have any additional questions, feel free to join the [discord support server](https://discord.gg/8hPrwAH). diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..72fd1d4 --- /dev/null +++ b/deno.json @@ -0,0 +1,12 @@ +{ + "name": "@matmen/imagescript", + "version": "1.4.0", + "exports": "./mod.ts", + "imports": { + "@std/bytes": "jsr:@std/bytes@^1.0.0", + "env": "./utils/wasm/env.js" + }, + "publish": { + "exclude": ["tests/"] + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..5ebf303 --- /dev/null +++ b/deno.lock @@ -0,0 +1,19 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@std/bytes@^1.0.0": "jsr:@std/bytes@1.0.0" + }, + "jsr": { + "@std/bytes@1.0.0": { + "integrity": "9392e72af80adccaa1197912fa19990ed091cb98d5c9c4344b0c301b22d7c632" + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@std/bytes@^1.0.0" + ] + } +} diff --git a/example.js b/example.js new file mode 100644 index 0000000..11f45e4 --- /dev/null +++ b/example.js @@ -0,0 +1,8 @@ +import {Image} from "jsr:@matmen/imagescript"; + +const input = await Deno.readFile('./tests/targets/readme.png'); +const image = await Image.decode(input); +image.rotate(180); + +const output = await image.encode(); +await Deno.writeFile('./output.png', output); \ No newline at end of file diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..b1ab49e --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export * from './ImageScript.js'; \ No newline at end of file diff --git a/package.json b/package.json index 1fbdc12..5c4da0b 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,3 @@ { - "name": "imagescript", - "version": "1.1.5", - "description": "zero-dependency javascript image manipulation", - "main": "ImageScript.js", - "scripts": { - "test": "node ./tests/run.js", - "coverage": "nyc --reporter=html npm test" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/matmen/ImageScript.git" - }, - "keywords": [ - "image", - "image processing", - "image manipulation" - ], - "author": "matmen ", - "license": "GPL-3.0-or-later", - "bugs": { - "url": "https://github.com/matmen/ImageScript/issues" - }, - "homepage": "https://github.com/matmen/ImageScript#readme", - "engines": { - "node": ">=12.0.0" - } + "types": "ImageScript.d.ts" } diff --git a/png/README.md b/png/README.md new file mode 100644 index 0000000..2fd9f95 --- /dev/null +++ b/png/README.md @@ -0,0 +1 @@ +TBD \ No newline at end of file diff --git a/png/src/crc.mjs b/png/src/crc.mjs new file mode 100644 index 0000000..131b39e --- /dev/null +++ b/png/src/crc.mjs @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..ce6114e --- /dev/null +++ b/png/src/mem.mjs @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..62848d4 --- /dev/null +++ b/png/src/png.mjs @@ -0,0 +1,254 @@ +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 new file mode 100644 index 0000000..93f331b --- /dev/null +++ b/png/src/zlib.mjs @@ -0,0 +1,618 @@ +// 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/tests/averageColor.js b/tests/averageColor.js index 6d34582..0b4627e 100644 --- a/tests/averageColor.js +++ b/tests/averageColor.js @@ -1,8 +1,8 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; + (async () => { - const binary = await fs.readFile('./tests/targets/readme.png'); + const binary = await Deno.readFile('./tests/targets/readme.png'); const image = await Image.decode(binary); const avgColor = image.averageColor(); - if (avgColor !== 0x343c3dff) process.exit(1); + if (avgColor !== 0x333c3dff) Deno.exit(1); })(); \ No newline at end of file diff --git a/tests/circle.js b/tests/circle.js index bfd46ca..abead8b 100644 --- a/tests/circle.js +++ b/tests/circle.js @@ -1,5 +1,5 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; (async () => { { @@ -8,8 +8,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/circle.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/circle.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -18,8 +18,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/circle2.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/circle2.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -29,8 +29,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/circle3.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/circle3.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -40,7 +40,7 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/circle4.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/circle4.png'); + if (!equals(encoded, target)) process.exit(1); } })(); \ No newline at end of file diff --git a/tests/crop.js b/tests/crop.js index a403668..03bebfc 100644 --- a/tests/crop.js +++ b/tests/crop.js @@ -1,5 +1,5 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; (async () => { const image = new Image(512, 512); @@ -8,6 +8,6 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/crop.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/crop.png'); + if (!equals(encoded, target)) process.exit(1); })(); \ No newline at end of file diff --git a/tests/decode.js b/tests/decode.js index a20070f..9a9be6a 100644 --- a/tests/decode.js +++ b/tests/decode.js @@ -1,29 +1,37 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); -const ImageTest = require('./image'); +import {Image} from '../ImageScript.js'; +import * as ImageTest from './image.js'; +import {equals} from "@std/bytes"; const panic = message => { console.error(message); - process.exit(1); + Deno.exit(1); }; (async () => { { - const binary = await fs.readFile('./tests/targets/image.png'); + const binary = await Deno.readFile('./tests/targets/image.png'); const image = await Image.decode(binary); - const target = await ImageTest; - if (!Buffer.from(target.bitmap).equals(Buffer.from(image.bitmap))) process.exit(1); + const target = await ImageTest.default; + if (!equals(target.bitmap, image.bitmap)) panic('arrays are unequal'); } { - const binary = await fs.readFile('./tests/targets/external.png'); + const binary = await Deno.readFile('./tests/targets/external.png'); const image = await Image.decode(binary); if ([image.width, image.height].some(v => v !== 638)) panic('dimensions don\'t match'); - if (!Buffer.from(image.bitmap.subarray(0, 4)).equals(Buffer.from([70, 65, 62, 255]))) + if (!equals(image.bitmap.subarray(0, 4), Uint8Array.from([70, 65, 62, 255]))) panic('pixel doesn\'t match'); } + + { + const binary = await Deno.readFile('./tests/targets/external.jpg'); + const image = await Image.decode(binary); + + if ([image.width, image.height].some(v => v !== 638)) + panic('jpeg dimensions don\'t match'); + } })(); \ No newline at end of file diff --git a/tests/fill.js b/tests/fill.js index 57220b1..d570bcd 100644 --- a/tests/fill.js +++ b/tests/fill.js @@ -1,5 +1,5 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; const panic = message => { console.error(message); @@ -13,8 +13,8 @@ const panic = message => { const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/fill-static.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) panic('fill static doesn\'t equal'); + const target = await Deno.readFile('./tests/targets/fill-static.png'); + if (!equals(encoded, target)) panic('fill static doesn\'t equal'); } { @@ -23,7 +23,7 @@ const panic = message => { const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/fill-func.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) panic('fill func doesn\'t equal'); + const target = await Deno.readFile('./tests/targets/fill-func.png'); + if (!equals(encoded, target)) panic('fill func doesn\'t equal'); } })(); \ No newline at end of file diff --git a/tests/fisheye.js b/tests/fisheye.js new file mode 100644 index 0000000..6cb3583 --- /dev/null +++ b/tests/fisheye.js @@ -0,0 +1,15 @@ +import {Image} from '../ImageScript.js'; +import {equals} from '@std/bytes'; + +(async () => { + const input = await Deno.readFile('./tests/targets/external.png'); + + const image = await Image.decode(input); + image.fisheye(); + + const output = await image.encode(); + const target = await Deno.readFile('./tests/targets/fisheye.png'); + + if (!equals(output, target)) + process.exit(1); +})(); diff --git a/tests/flip.js b/tests/flip.js new file mode 100644 index 0000000..2352e5f --- /dev/null +++ b/tests/flip.js @@ -0,0 +1,30 @@ +import {Image} from '../ImageScript.js'; +import {equals} from '@std/bytes'; + +(async () => { + const input = await Deno.readFile('./tests/targets/external.png'); + + { + const image = await Image.decode(input); + image.flip('horizontal'); + + const output = await image.encode(1, {creationTime: 0, software: ''}); + + const target = await Deno.readFile('./tests/targets/flip-horizontal.png'); + + if (!equals(output, target)) + process.exit(1); + } + + { + const image = await Image.decode(input); + image.flip('vertical'); + + const output = await image.encode(1, {creationTime: 0, software: ''}); + + const target = await Deno.readFile('./tests/targets/flip-vertical.png'); + + if (!equals(output, target)) + process.exit(1); + } +})(); diff --git a/tests/font.js b/tests/font.js index 4151c3d..aaa956f 100644 --- a/tests/font.js +++ b/tests/font.js @@ -4,8 +4,8 @@ https://www.1001fonts.com/ethnocentric-font.html */ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; const panic = message => { console.error(message); @@ -14,18 +14,18 @@ const panic = message => { (async () => { { - const font = await Image.renderText(await fs.readFile('./tests/fonts/carbon phyber.ttf'), 128, 'ThE qUiCk'); + const font = await Image.renderText(await Deno.readFile('./tests/fonts/carbon phyber.ttf'), 128, 'ThE qUiCk'); const encoded = await font.encode(); - const desired = await fs.readFile('./tests/targets/font-1.png'); - if (!desired.equals(Buffer.from(encoded))) panic('font 1 doesn\'t match'); + const desired = await Deno.readFile('./tests/targets/font-1.png'); + if (!equals(desired, encoded)) panic('font 1 doesn\'t match'); } { - const font = await Image.renderText(await fs.readFile('./tests/fonts/ethnocentric rg.ttf'), 128, 'BrOwN fOx'); + const font = await Image.renderText(await Deno.readFile('./tests/fonts/ethnocentric rg.ttf'), 128, 'BrOwN fOx'); const encoded = await font.encode(); - const desired = await fs.readFile('./tests/targets/font-2.png'); - if (!desired.equals(Buffer.from(encoded))) panic('font 2 doesn\'t match'); + const desired = await Deno.readFile('./tests/targets/font-2.png'); + if (!equals(desired, encoded)) panic('font 2 doesn\'t match'); } })(); \ No newline at end of file diff --git a/tests/gif.js b/tests/gif.js index 214123b..7857c44 100644 --- a/tests/gif.js +++ b/tests/gif.js @@ -1,17 +1,41 @@ -const fs = require('fs').promises; -const {Frame, GIF} = require('../ImageScript'); +import {Frame, GIF} from '../ImageScript.js'; +import {equals} from '@std/bytes'; + +const panic = message => { + console.error(message); + process.exit(1); +}; (async () => { const frames = []; for (let i = 0; i < 30; i++) { - const frame = Frame.new(128, 128); + const frame = new Frame(256, 256); frame.fill(x => Frame.hslToColor(x / frame.width + i / 30, 1, 0.5)); frames.push(frame); } const gif = new GIF(frames); - const encoded = await gif.encode(); - const desired = await fs.readFile('./tests/targets/gif.gif'); - process.exit(desired.equals(encoded) ? 0 : 1); -})(); \ No newline at end of file + { + const encoded = await gif.encode(); + const desired = await Deno.readFile('./tests/targets/gif.gif'); + if (!equals(desired, encoded)) + panic('encoding failed'); + } + + { + const binary = await Deno.readFile('./tests/targets/gif.gif'); + const decoded = await GIF.decode(binary); + if (decoded.width !== gif.width) + panic('decode: incorrect width'); + if (decoded.height !== gif.height) + panic('decode: incorrect width'); + if (decoded.length !== gif.length) + panic('decode: incorrect frame count'); + + for (let i = 0; i < gif.length; i++) { + if (gif[i].bitmap.length !== decoded[i].bitmap.length) + panic('decode: incorrect frame bitmap length'); + } + } +})(); diff --git a/tests/image.js b/tests/image.js index 773a9cf..047ec91 100644 --- a/tests/image.js +++ b/tests/image.js @@ -1,13 +1,13 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; -module.exports = (async () => { +export default (async () => { const image = new Image(128, 128); image.fill(x => Image.hslToColor(x / image.width, 1, 0.5)); const encoded = await image.encode(); - const desired = await fs.readFile('./tests/targets/image.png'); - if (process.argv[1].slice(-8) === 'image.js') process.exit(desired.equals(encoded) ? 0 : 1); + const desired = await Deno.readFile('./tests/targets/image.png'); + if (import.meta.main) Deno.exit(equals(desired, encoded) ? 0 : 1); return image; })(); \ No newline at end of file diff --git a/tests/invert.js b/tests/invert.js index 9442cdd..52b90bb 100644 --- a/tests/invert.js +++ b/tests/invert.js @@ -1,5 +1,5 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; (async () => { { @@ -9,8 +9,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/invert.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/invert.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -20,8 +20,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/invert-value.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/invert-value.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -31,8 +31,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/invert-saturation.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/invert-saturation.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -42,8 +42,8 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/invert-hue.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/invert-hue.png'); + if (!equals(encoded, target)) process.exit(1); } { @@ -53,7 +53,7 @@ const {Image} = require('../ImageScript'); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/invert-hueshift.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/invert-hueshift.png'); + if (!equals(encoded, target)) process.exit(1); } })(); \ No newline at end of file diff --git a/tests/jpeg.js b/tests/jpeg.js index 882823e..9650cb2 100644 --- a/tests/jpeg.js +++ b/tests/jpeg.js @@ -1,5 +1,5 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; const panic = message => { console.error(message); @@ -7,12 +7,12 @@ const panic = message => { }; (async () => { - const binary = await fs.readFile('./tests/targets/external.jpg'); + const binary = await Deno.readFile('./tests/targets/external.jpg'); const image = await Image.decode(binary); if ([image.width, image.height].some(v => v !== 638)) panic('dimensions don\'t match'); - if (!Buffer.from(image.bitmap.slice(0, 4)).equals(Buffer.from([70, 65, 61, 255]))) + if (!equals(image.bitmap.slice(0, 4), [70, 65, 61, 255])) panic('pixel doesn\'t match'); await image.encodeJPEG(100); diff --git a/tests/misc.js b/tests/misc.js index 99c7f13..2a23dee 100644 --- a/tests/misc.js +++ b/tests/misc.js @@ -1,4 +1,6 @@ -const {Image, Frame, GIF} = require('../ImageScript'); +import {Frame, GIF, Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; + const panic = msg => { console.error(msg); process.exit(1); @@ -103,7 +105,7 @@ if (Image.hslaToColor(0, 0, 1, 0xff) !== 0xffffffff) const clone = image.clone(); - if (!Buffer.from(clone.bitmap).equals(Buffer.from(image.bitmap))) + if (!equals(clone.bitmap, image.bitmap)) panic('clone failed'); } @@ -202,13 +204,6 @@ try { } catch { } -try { - const frame = new Frame(512, 512); - new GIF([frame], -1); - panic('gif frame instanceof failed'); -} catch { -} - { const frame = new Frame(512, 128, 123); const gif = new GIF([frame]); diff --git a/tests/readme.js b/tests/readme.js index 5ad7df1..36c9105 100644 --- a/tests/readme.js +++ b/tests/readme.js @@ -1,18 +1,19 @@ -const {Image} = require('..'); -const fs = require('fs').promises; +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; (async () => { + const decoder = new TextDecoder('utf-8'); const [backgroundSVG, avatarBinary, badges, font] = await Promise.all([ - fs.readFile('./tests/svgs/background.svg').then(b => b.toString()), - fs.readFile('./tests/targets/external.png'), + Deno.readFile('./tests/svgs/background.svg').then(b => decoder.decode(b)), + Deno.readFile('./tests/targets/external.png'), Promise.all( [ 'crown', 'potato', 'mask', 'microbe', 'petri_dish', 'thermometer', 'cigarette' ].map( - x => fs.readFile(`./tests/svgs/${x}.svg`) - .then(b => b.toString())) + x => Deno.readFile(`./tests/svgs/${x}.svg`) + .then(b => decoder.decode(b))) ), - fs.readFile('./tests/fonts/carbon phyber.ttf') + Deno.readFile('./tests/fonts/carbon phyber.ttf') ]); const image = new Image(1000, 700); @@ -103,6 +104,6 @@ const fs = require('fs').promises; const encoded = await image.encode(); - if (!(await fs.readFile('./tests/targets/readme.png')).equals(Buffer.from(encoded))) - process.exit(1); + if (!equals(await Deno.readFile('./tests/targets/readme.png'), encoded)) + Deno.exit(1); })(); \ No newline at end of file diff --git a/tests/resize.js b/tests/resize.js index f477d3e..d834e27 100644 --- a/tests/resize.js +++ b/tests/resize.js @@ -1,11 +1,12 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; + (async () => { - const binary = await fs.readFile('./tests/targets/image.png'); - const image = await Image.decode(binary); - image.resize(image.width / 4, Image.RESIZE_AUTO); + const binary = await Deno.readFile('./tests/targets/image.png'); + const image = await Image.decode(binary); + image.resize(image.width / 4, Image.RESIZE_AUTO); - const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/resize.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const encoded = await image.encode(); + const target = await Deno.readFile('./tests/targets/resize.png'); + if (!equals(encoded, target)) process.exit(1); })(); \ No newline at end of file diff --git a/tests/rotate.js b/tests/rotate.js index a8742c0..5f8dc5f 100644 --- a/tests/rotate.js +++ b/tests/rotate.js @@ -1,5 +1,6 @@ -const fs = require('fs').promises; -const {Image} = require('../ImageScript'); +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; + const panic = msg => { console.error(msg); process.exit(1); @@ -7,45 +8,42 @@ const panic = msg => { (async () => { { - const binary = await fs.readFile('./tests/targets/image.png'); + const binary = await Deno.readFile('./tests/targets/image.png'); const image = await Image.decode(binary); image.rotate(45); const encoded = await image.encode(); - await fs.writeFile('./tests/targets/rotate-45.png', encoded); - const target = await fs.readFile('./tests/targets/rotate-45.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) panic('rotate 45 failed'); + const target = await Deno.readFile('./tests/targets/rotate-45.png'); + if (!equals(encoded, target)) panic('rotate 45 failed'); } { - const binary = await fs.readFile('./tests/targets/image.png'); + const binary = await Deno.readFile('./tests/targets/image.png'); const image = await Image.decode(binary); image.rotate(45, false); const encoded = await image.encode(); - await fs.writeFile('./tests/targets/rotate-45-noresize.png', encoded); - const target = await fs.readFile('./tests/targets/rotate-45-noresize.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) panic('rotate 45 noresize failed'); + const target = await Deno.readFile('./tests/targets/rotate-45-noresize.png'); + if (!equals(encoded, target)) panic('rotate 45 noresize failed'); } { - const binary = await fs.readFile('./tests/targets/image.png'); + const binary = await Deno.readFile('./tests/targets/image.png'); const image = await Image.decode(binary); image.rotate(180); const encoded = await image.encode(); - await fs.writeFile('./tests/targets/rotate-180.png', encoded); - const target = await fs.readFile('./tests/targets/rotate-180.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) panic('rotate 180 failed'); + const target = await Deno.readFile('./tests/targets/rotate-180.png'); + if (!equals(encoded, target)) panic('rotate 180 failed'); } { const image = new Image(512, 512); image.fill((x) => Image.hslToColor(x / image.width, 1, .5)); - if (!Buffer.from(image.bitmap).equals(Buffer.from(image.rotate(360).bitmap))) + if (!equals(image.bitmap, image.rotate(360).bitmap)) panic('rotate 360 failed'); } })(); \ No newline at end of file diff --git a/tests/run.js b/tests/run.js index 20fb3fa..f0bc106 100644 --- a/tests/run.js +++ b/tests/run.js @@ -1,30 +1,29 @@ -const fs = require('fs'); -const child_process = require('child_process'); - (async () => { - for (const file of fs.readdirSync('./tests/')) { + for await (const {name: file} of Deno.readDir('./tests/')) { if (file === 'run.js' || file.slice(-3) !== '.js') continue; console.log(`running test ${file}`); const start = Date.now(); - const proc = child_process.exec(`node --unhandled-rejections=strict ./tests/${file}`); - proc.stderr.pipe(process.stderr); - proc.stdout.pipe(process.stdout); + const proc = Deno.run({ + cmd: ['deno', 'run', '-A', `./tests/${file}`], + stdout: 'piped', + stderr: 'piped' + }); + await new Promise(resolve => { const timeout = setTimeout(() => { - if (proc.connected) { - console.log('script timeout'); - proc.exitCode = 1; - proc.kill('SIGTERM'); - } - }, 1000); + console.log('script timeout'); + proc.exitCode = 1; + proc.kill(15); + }, 5000); - proc.on('exit', code => { + proc.status().then(async ({code}) => { clearTimeout(timeout); if (code) { + await Deno.stdout.write(await proc.stderrOutput()); console.error(`test ${file} failed in ${Date.now() - start}ms`); - process.exit(1); + Deno.exit(1); } else { console.log(`test ${file} passed in ${Date.now() - start}ms`); resolve(); diff --git a/tests/svg.js b/tests/svg.js index 3cba8ee..68de5be 100644 --- a/tests/svg.js +++ b/tests/svg.js @@ -1,11 +1,13 @@ -const {Image} = require('../ImageScript'); -const fs = require('fs').promises; +import {Image} from '../ImageScript.js'; +import {equals} from "@std/bytes"; (async () => { - const svg = await fs.readFile('./tests/svgs/potato.svg'); - const image = await Image.renderSVG(svg.toString(), 256 / 36, Image.SVG_MODE_SCALE); + const decoder = new TextDecoder('utf-8'); + + const svg = await Deno.readFile('./tests/svgs/potato.svg'); + const image = Image.renderSVG(decoder.decode(svg), 256 / 36, Image.SVG_MODE_SCALE); const encoded = await image.encode(); - const target = await fs.readFile('./tests/targets/potato.png'); - if (!Buffer.from(target).equals(Buffer.from(encoded))) process.exit(1); + const target = await Deno.readFile('./tests/targets/potato.png'); + if (!equals(encoded, target)) Deno.exit(1); })(); \ No newline at end of file diff --git a/tests/targets/circle3.png b/tests/targets/circle3.png index a137b4b..8c3c517 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 d1ea6b4..a1254e1 100644 Binary files a/tests/targets/circle4.png and b/tests/targets/circle4.png differ diff --git a/tests/targets/fisheye.png b/tests/targets/fisheye.png new file mode 100644 index 0000000..7d4c418 Binary files /dev/null and b/tests/targets/fisheye.png differ diff --git a/tests/targets/flip-horizontal.png b/tests/targets/flip-horizontal.png new file mode 100644 index 0000000..5e6e656 Binary files /dev/null and b/tests/targets/flip-horizontal.png differ diff --git a/tests/targets/flip-vertical.png b/tests/targets/flip-vertical.png new file mode 100644 index 0000000..630fd5d Binary files /dev/null and b/tests/targets/flip-vertical.png differ diff --git a/tests/targets/gif.gif b/tests/targets/gif.gif index d494ab6..a4a9c95 100644 Binary files a/tests/targets/gif.gif and b/tests/targets/gif.gif differ diff --git a/tests/targets/potato.png b/tests/targets/potato.png index 985b077..a141c6e 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 2261a2d..f09a9bc 100644 Binary files a/tests/targets/readme.png and b/tests/targets/readme.png differ diff --git a/tests/targets/rotate-45-noresize.png b/tests/targets/rotate-45-noresize.png index aa00020..c317f28 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 9fd95f5..fe6d62a 100644 Binary files a/tests/targets/rotate-45.png and b/tests/targets/rotate-45.png differ diff --git a/utils/buffer.js b/utils/buffer.js index 04c0318..0689256 100644 --- a/utils/buffer.js +++ b/utils/buffer.js @@ -1,4 +1,4 @@ -module.exports = class Buffer { +export class Buffer { static concat(...arrays) { const array = new Uint8Array( arrays.reduce((length, array) => length + array.length, 0) diff --git a/utils/crc32.js b/utils/crc32.js index c1da781..504503f 100644 --- a/utils/crc32.js +++ b/utils/crc32.js @@ -43,7 +43,7 @@ const table = new Uint32Array([ 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D]); -module.exports = function crc32(buffer) { +export function crc32(buffer) { let offset = 0; let crc = 0xFFFFFFFF; @@ -59,4 +59,4 @@ module.exports = function crc32(buffer) { } return (crc ^ 0xFFFFFFFF) >>> 0; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/utils/gif.js b/utils/gif.js deleted file mode 100644 index 46a3725..0000000 --- a/utils/gif.js +++ /dev/null @@ -1,231 +0,0 @@ -const HEAD = Uint8Array.of(0x47, 0x49, 0x46, 0x38, 0x39, 0x61); -const AEB = Uint8Array.of(0x21, 0xFF, 0x0B, 0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2E, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00); - -module.exports = function encode(frames, opts) { - let size = 0; - - for (const frame of frames) { - frame.size = 1; - let r_offset = 0; - let p_offset = 0; - const table = frame.table = new Map; - const refs = new Uint8Array(frame.width * frame.height); - - while (p_offset < frame.pixels.length) { - const slice = frame.pixels.subarray(p_offset, p_offset += 4); - - if (0 === slice[3]) refs[r_offset++] = 0; - else { - const hash = slice[2] | (slice[1] << 8) | (slice[0] << 16); - - const prev = table.get(hash); - if (prev) refs[r_offset++] = prev; - - else if (256 > frame.size) { - table.set(hash, frame.size); - refs[r_offset++] = frame.size++; - } else refs[r_offset++] = find_nearest(table, slice); - } - } - - frame.bits = Math.max(2, Math.ceil(Math.log2(frame.size))); - size += 3 * (1 << frame.bits) + (frame.compressed = compress(refs, frame.bits)).length; - } - - const buffer = new ArrayBuffer(size - + 1 // END - + 8 // LSD - + AEB.length // AEB - + HEAD.length // HEAD - + (4 + opts.comment.length) // CE - + ((1 + 1 + 8 + 10) * frames.length) // frames metadata - ); - - const u8 = new Uint8Array(buffer); - const view = new DataView(buffer); - - // HB 0:5 - u8.set(HEAD, 0); - - // LSD 6:12 - view.setUint16(6, opts.width, true); - view.setUint16(8, opts.height, true); - // 10:12 - (GCT flag, color resolution, sort flag, size of GCT), (background color index), (pixel aspect ratio) - - let offset = 13; - // GCT if (GCT_f) - - // AEB - u8.set(AEB, offset); - offset += AEB.length; - view.setUint16(offset - 3, opts.loop, true); - - for (const frame of frames) { - // GCE 8b - u8[offset++] = 0x21; - u8[offset++] = 0xF9; - u8[offset++] = 0x04; - u8[offset++] = 0b00000001; - view.setUint16(offset, frame.delay / 10, true); - - offset += 2; - u8[offset++] = 0x00; - u8[offset++] = 0x00; - - // ID 10b - u8[offset++] = 0x2C; - view.setUint16(4 + offset, frame.width, true); - view.setUint16(6 + offset, frame.height, true); - - offset += 8; - u8[offset++] = 0x00 | (1 << 7) | ((frame.bits - 1) & 0x07); - - const color_table = new Uint8Array(3 * (1 << frame.bits)); - - for (const pair of frame.table) { - const t_offset = 3 * pair[1]; - color_table[t_offset] = pair[0] >> 16; - color_table[2 + t_offset] = pair[0] & 0xff; - color_table[1 + t_offset] = pair[0] >> 8 & 0xff; - } - - // LCT - u8.set(color_table, offset); - offset += color_table.length; - - // IDa - u8[offset++] = frame.bits; - u8.set(frame.compressed, offset); - offset += frame.compressed.length; - - u8[offset++] = 0x00; - } - - // CE - u8[offset++] = 0x21; - u8[offset++] = 0xFE; - u8[offset++] = opts.comment.length; - - u8.set(opts.comment, offset); - offset += opts.comment.length; - - u8[offset++] = 0x00; - return (u8[offset] = 0x3B, u8); -}; - -function find_nearest(table, slice) { - let index = 0; - let distance = Infinity; - - for (const pair of table) { - const hypot = Math.hypot( - slice[0] - pair[0] >> 16, - slice[2] - pair[0] & 0xff, - slice[1] - pair[0] >> 8 & 0xff, - ); - - if (hypot < distance) { - index = pair[1]; - distance = hypot; - } - } - - return index; -} - -const BITS = 8; -const LENGTH = 12; -const LIMIT = 1 << BITS; - -class blocks_buffer extends Array { - constructor() { - super(); - this.byte = 0; - this.offset = 0; - this.b_offset = 0; - this.block = new Uint8Array(LIMIT); - } - - - u8() { - const u8 = new Uint8Array(this.length + super.reduce((length, array) => length + array.length, 0)); - - let offset = 0; - for (const block of this) { - u8[offset++] = block.length; - - u8.set(block, offset); - offset += block.length; - } - - return u8; - } - - pack(code, length) { - if (LIMIT <= this.b_offset + (this.offset ? 1 : 0) + Math.ceil(length / BITS)) this.new_block(); - - let offset = 0; - while (offset < length) { - this.byte |= ((code >> offset++) & 1) << this.offset++; - if (BITS === this.offset) (this.block[this.b_offset++] = this.byte, this.byte = 0, this.offset = 0); - } - } - - new_block() { - if (0 === this.b_offset) return; - super.push(this.block.slice(0, this.b_offset)); - - this.b_offset = 0; - } - - end() { - if (0 !== this.offset) this.block[this.b_offset++] = (this.byte); - - super.push(this.block.subarray(0, this.b_offset)); - } -} - -function compress(refs, bits) { - const clear_code = 1 << bits; - const end_code = 1 + clear_code; - - let s = ''; - let code_len = 1 + bits; - let dictionary = new Map; - const buffer = new blocks_buffer; - let dictionary_len = 1 + end_code; - buffer.pack(clear_code, code_len); - - for (const ref of refs) { - const char = String.fromCharCode(ref); - - const key = s + char; - if (1 === key.length || dictionary.has(key)) s = key; - else { - if (1 < s.length) buffer.pack(dictionary.get(s), code_len); - else buffer.pack(s.charCodeAt(0), code_len); - - dictionary.set(key, dictionary_len++); - code_len = Math.ceil(Math.log2(dictionary_len)); - - if ((1 << LENGTH) <= dictionary_len) { - dictionary = new Map; - dictionary_len = 1 + end_code; - buffer.pack(clear_code, code_len); - code_len = Math.ceil(Math.log2(dictionary_len)); - } - - s = char; - } - } - - if (s !== '') { - if (1 < s.length) buffer.pack(dictionary.get(s), code_len); - else buffer.pack(s.charCodeAt(0), code_len); - } - - buffer.pack(end_code, code_len); - - buffer.end(); - return buffer.u8(); -} diff --git a/utils/png.js b/utils/png.js index 81773cc..9259312 100644 --- a/utils/png.js +++ b/utils/png.js @@ -1,7 +1,7 @@ /* global SharedArrayBuffer */ -const crc32 = require('./crc32.js'); -const Buffer = require('./buffer'); -const {compress, decompress} = require('./wasm/zlib.js'); +import {crc32} from './crc32.js'; +import {Buffer} from './buffer.js'; +import {compress, decompress} from './wasm/zlib.js'; const __IHDR__ = new Uint8Array([73, 72, 68, 82]); const __IDAT__ = new Uint8Array([73, 68, 65, 84]); @@ -25,189 +25,199 @@ const channels_to_color_type = { 4: color_types.TRUECOLOR_ALPHA }; -module.exports = { - async encode(data, {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); +export async function encode(data, {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); + while (offset < data.length) { + tmp[tmp_offset++] = 0; + tmp.set(data.subarray(offset, (offset += row_length)), tmp_offset); - tmp_offset += row_length; + tmp_offset += row_length; + } + + const compressed = await compress(tmp, level); + const array = new Uint8Array(49 + 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.set(__IEND__, 49 + compressed.length); + array[25] = channels_to_color_type[channels]; + + 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 + compressed.length, 0); + view.setUint32(53 + 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 async 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; + let type; + while (type !== 1229278788) { + type = view.getUint32(4 + c_offset); + + // IDAT + if (type === 1229209940) + 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]; } - const compressed = await compress(tmp, level); - const array = new Uint8Array(49 + 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.set(__IEND__, 49 + compressed.length); - array[25] = channels_to_color_type[channels]; - - 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 + compressed.length, 0); - view.setUint32(53 + 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; - }, - async 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; - if (array[25] === 3) - palette = new Uint32Array(2 ** bpc); - - let type; - while (type !== 1229278788) { - type = view.getUint32(4 + c_offset); - - // IDAT - if (type === 1229209940) - chunks.push(array.subarray(8 + c_offset, 8 + c_offset + view.getUint32(c_offset))); - else if (type === 1347179589) { - 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; - } + c_offset += 4 + 4 + 4 + view.getUint32(c_offset); + } - c_offset += 4 + 4 + 4 + view.getUint32(c_offset); - } + array = await decompress(chunks.length === 1 ? chunks[0] : Buffer.concat(...chunks)); - array = await decompress(chunks.length === 1 ? chunks[0] : Buffer.concat(...chunks)); + while (offset < array.byteLength) { + const filter = array[offset++]; + const slice = array.subarray(offset, offset += 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); - if (0 === filter) pixels.set(slice, p_offset); - else if (1 === filter) this.filter_1(slice, pixels, p_offset, bytespp, row_length); - else if (2 === filter) this.filter_2(slice, pixels, p_offset, bytespp, row_length); - else if (3 === filter) this.filter_3(slice, pixels, p_offset, bytespp, row_length); - else if (4 === filter) this.filter_4(slice, pixels, p_offset, bytespp, row_length); + p_offset += row_length; + } - p_offset += row_length; - } + if (pixel_type === 3) { + if (!palette) + throw new Error('Indexed color PNG has no PLTE'); - if (channels === 1 && palette) { - 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 (alphaPalette) + for (let i = 0; i < alphaPalette.length; i++) + palette[i] &= 0xffffff00 | alphaPalette[i]; - 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; - } + 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 (channels !== 4) { - const newPixels = new Uint8Array(width * height * 4); - const view = new DataView(newPixels.buffer); + 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 === 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) { + 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); - } + } 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; - } + pixels = newPixels; + } return {width, height, pixels}; - }, + } - 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]; - }, - - 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_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]; +} - filter_3(slice, pixels, p_offset, bytespp, row_length) { +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]; + } +} - 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_3(slice, pixels, p_offset, bytespp, row_length) { + let i = 0; - 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] >> 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); + } +} - 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]; +function filter_4(slice, pixels, p_offset, bytespp, row_length) { + let i = 0; - 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]; + 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]; - const p = a + b - c; - const pa = Math.abs(p - a); - const pb = Math.abs(p - b); - const pc = Math.abs(p - c); + 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]; - pixels[i + p_offset] = slice[i++] + ((pa <= pb && pa <= pc) ? a : ((pb <= pc) ? b : c)); - } + 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)); } } -}; \ No newline at end of file +} diff --git a/utils/wasm/env.js b/utils/wasm/env.js new file mode 100644 index 0000000..8f5461b --- /dev/null +++ b/utils/wasm/env.js @@ -0,0 +1,7 @@ +export const streams = new Map; + +export function emscripten_notify_memory_growth(...args) { console.log(...args) } +export function push_to_stream(id, ptr) { + const mem = streams.get(id).m; + streams.get(id).cb(mem.u8(ptr, mem.length()).slice()); +} \ No newline at end of file diff --git a/utils/wasm/font.js b/utils/wasm/font.js index fc3c753..1cd8386 100644 --- a/utils/wasm/font.js +++ b/utils/wasm/font.js @@ -1,98 +1,122 @@ -const {join} = require('path'); -const {promises: {readFile}} = require('fs'); +import * as wasm from './font.wasm'; -let u8array_ref, i32array_ref, u32array_ref, wasm; +let registry; -const utf8encoder = new TextEncoder(); +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); } -function u8array() { - return u8array_ref.buffer === wasm.memory.buffer ? u8array_ref : (u8array_ref = new Uint8Array(wasm.memory.buffer)); + static copy_and_free(ptr, size) { + let slice = mem.u8(ptr, size).slice(); + return (wasm.wfree(ptr, size), slice); + } } -function i32array() { - return i32array_ref.buffer === wasm.memory.buffer ? i32array_ref : (i32array_ref = new Int32Array(wasm.memory.buffer)); -} +const decode_utf8 = globalThis.Deno?.core?.decode ?? TextDecoder.prototype.decode.bind(new TextDecoder); +const encode_utf8 = globalThis.Deno?.core?.encode ?? globalThis.Buffer?.from.bind(globalThis.Buffer) ?? TextEncoder.prototype.encode.bind(new TextEncoder); -function u32array() { - return u32array_ref.buffer === wasm.memory.buffer ? u32array_ref : (u32array_ref = new Uint32Array(wasm.memory.buffer)); +if ('FinalizationRegistry' in globalThis) { + registry = new FinalizationRegistry(([t, ptr]) => { + if (!ref.deref()) return; + if (t === 0) wasm.font_free(ptr); + if (t === 1) wasm.layout_free(ptr); + }); } -function ptr_to_u8array(ptr, len) { - return u8array().subarray(ptr, ptr + len); -} +export class Font { + constructor(scale, buffer) { + this._w = wasm; + 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); -function ptr_to_u32array(ptr, len) { - return u32array().subarray(ptr / 4, ptr / 4 + len); -} + if (!this.ptr) throw new Error('invalid font'); + if (registry) registry.register(this, [0, this.ptr], this); + } -function u8array_to_ptr(buffer) { - const ptr = wasm.__wbindgen_malloc(buffer.length); - u8array().set(buffer, ptr); + free() { + this.ptr = wasm.font_free(this.ptr); + if (registry) registry.unregister(this); + } - return ptr; -} + has(char) { + return wasm.font_has(this.ptr, String.prototype.charCodeAt.call(char, 0)); + } -function string_to_ptr(string) { - let offset = 0; - let len = string.length; - let ptr = wasm.__wbindgen_malloc(string.length); + 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()))); - const u8 = u8array(); - while (len > offset) { - const code = string.charCodeAt(offset); + return (wasm.font_metrics_free(ptr), metrics); + } - if (code > 0x7F) break; - u8[ptr + offset++] = code; - } - - if (offset !== len) { - if (offset !== 0) string = string.substring(offset); - ptr = wasm.__wbindgen_realloc(ptr, len, len = offset + string.length * 3); - const ret = utf8encoder.encodeInto(string, u8array().subarray(ptr + offset, ptr + len)); + rasterize(char, scale = this.scale) { + const ptr = wasm.font_rasterize(this.ptr, String.prototype.charCodeAt.call(char, 0), scale); - offset += ret.written; + 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 [ptr, offset]; + return (wasm.font_rasterize_free(ptr), glyph); + } } -const nullish = x => x == null; - -module.exports = { - render(ptr, id, scale, r, g, b, text, max_width, wrap_style = false) { - const str = string_to_ptr(text); - wasm.render(ptr, id, scale, r, g, b, str[0], str[1], !nullish(max_width), max_width || 0, wrap_style); - }, - buffer(id) { - wasm.buffer(8, id); - const i32 = i32array(); - const slice = ptr_to_u8array(i32[2], i32[3]).slice(); - wasm.__wbindgen_free(i32[2], i32[3]); - - return slice; - }, - meta(id) { - wasm.meta(8, id); - const i32 = i32array(); - const slice = ptr_to_u32array(i32[2], i32[3]).slice(); - wasm.__wbindgen_free(i32[2], 4 * i32[3]); - - return slice; - }, - async load(id, buffer, scale = 128) { - if (!wasm) { - const module = new WebAssembly.Module(await readFile(join(__dirname, './font.wasm'))); - const instance = new WebAssembly.Instance(module); - - wasm = instance.exports; - u8array_ref = new Uint8Array(wasm.memory.buffer); - i32array_ref = new Int32Array(wasm.memory.buffer); - u32array_ref = new Uint32Array(wasm.memory.buffer); - } - - wasm.load(id, u8array_to_ptr(buffer), buffer.length, scale); - }, - free(id) { - wasm.free(id); +export class Layout { + constructor() { + this._w = wasm; + if (registry) this.refs = []; + this.ptr = wasm.layout_new(); + 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 ?? font.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); + } } \ No newline at end of file diff --git a/utils/wasm/font.wasm b/utils/wasm/font.wasm index a03a568..01fdfc8 100644 Binary files a/utils/wasm/font.wasm and b/utils/wasm/font.wasm differ diff --git a/utils/wasm/gif.js b/utils/wasm/gif.js new file mode 100644 index 0000000..83b3c80 --- /dev/null +++ b/utils/wasm/gif.js @@ -0,0 +1,113 @@ +import * as wasm from './gif.wasm'; + +import {streams} from './env.js'; +const utf8encoder = new TextEncoder; + +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); + } +} + +export class Encoder { + constructor(width, height, loops = -1) { + this.k = Math.round(Math.random() * Number.MAX_SAFE_INTEGER); + this.m = mem; + this.slices = []; + this._w = wasm; + streams.set(0, this); + this.ptr = wasm.encoder_new(0, width, height, loops); + } + + cb(buffer) { + this.slices.push(buffer); + } + + free() { + this.ptr = wasm.encoder_free(this.ptr); + streams.delete(0); + } + + u8() { + this.free(); + let offset = 0; + const u8 = new Uint8Array(this.slices.reduce((sum, array) => sum + array.length, 0)); + + for (const x of this.slices) { + u8.set(x, offset); + offset += x.length; + } + + return u8; + } + + add(x, y, delay, width, height, buffer, dispose, quality) { + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + wasm.encoder_add(this.ptr, ptr, buffer.length, x, y, width, height, delay, dispose, quality); + } + + set comment(comment) { + const buffer = utf8encoder.encode(comment); + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + wasm.encoder_add_comment(this.ptr, ptr, buffer.length); + } + + set application(application) { + const buffer = utf8encoder.encode(application); + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + wasm.encoder_add_application(this.ptr, ptr, buffer.length); + } +} + +export class Decoder { + constructor(buffer, limit = 0) { + this._w = wasm; + 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); + } +} diff --git a/utils/wasm/gif.wasm b/utils/wasm/gif.wasm new file mode 100644 index 0000000..a66cfbf Binary files /dev/null and b/utils/wasm/gif.wasm differ diff --git a/utils/wasm/jpeg.js b/utils/wasm/jpeg.js index e905621..79997cc 100644 --- a/utils/wasm/jpeg.js +++ b/utils/wasm/jpeg.js @@ -1,137 +1,38 @@ -const {join} = require('path'); -const {promises: {readFile}} = require('fs'); - -let wasm; - -let cachegetUint8Memory0 = null; -function getUint8Memory0() { - if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { - cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachegetUint8Memory0; -} - -let WASM_VECTOR_LEN = 0; - -function passArray8ToWasm0(arg, malloc) { - const ptr = malloc(arg.length * 1); - getUint8Memory0().set(arg, ptr / 1); - WASM_VECTOR_LEN = arg.length; - return ptr; -} - -let cachegetInt32Memory0 = null; - -function getInt32Memory0() { - if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { - cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); - } - return cachegetInt32Memory0; +import * as wasm from './jpeg.wasm' + +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 getArrayU8FromWasm0(ptr, len) { - return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +export function encode(buffer, width, height, quality) { + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + return mem.copy_and_free(wasm.encode(ptr, width, height, quality), mem.length()); } -let cachegetUint16Memory0 = null; - -function getUint16Memory0() { - if (cachegetUint16Memory0 === null || cachegetUint16Memory0.buffer !== wasm.memory.buffer) { - cachegetUint16Memory0 = new Uint16Array(wasm.memory.buffer); - } - return cachegetUint16Memory0; -} - -function getArrayU16FromWasm0(ptr, len) { - return getUint16Memory0().subarray(ptr / 2, ptr / 2 + len); -} - -async function initWASM() { - if (wasm) return; - - const module = new WebAssembly.Module(await readFile(join(__dirname, './jpeg.wasm'))); - const instance = new WebAssembly.Instance(module); - wasm = instance.exports; -} +export 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); -module.exports = { - /** - * @param {number} width - * @param {number} height - * @param {number} quality - * @param {Uint8Array|Uint8ClampedArray} buffer - * @returns {Uint8Array} - */ - async encode(width, height, quality, buffer) { - await initWASM(); + if (0 === ptr) throw new Error('jpg: failed to decode'); + if (1 === ptr) throw new Error('jpg: failed to scale decoder'); - try { - const retptr = wasm.__wbindgen_export_0.value - 16; - wasm.__wbindgen_export_0.value = retptr; - const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc); - wasm.encode(retptr, width, height, quality, ptr0, WASM_VECTOR_LEN); - const r0 = getInt32Memory0()[retptr / 4]; - const r1 = getInt32Memory0()[retptr / 4 + 1]; - const v1 = getArrayU8FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 1); - return v1; - } finally { - wasm.__wbindgen_export_0.value += 16; - } - }, - /** - * @param {number} ptr - * @param {Uint8Array} buffer - * @param {number} width - * @param {number} height - * @returns {number} - */ - async decode(ptr, buffer, width, height) { - await initWASM(); + 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(), + } - const ptr0 = passArray8ToWasm0(buffer, wasm.__wbindgen_malloc); - return wasm.decode(ptr, ptr0, WASM_VECTOR_LEN, width, height); - }, - /** - * @param {number} id - * @returns {Uint16Array} - */ - meta(id) { - try { - const retptr = wasm.__wbindgen_export_0.value - 16; - wasm.__wbindgen_export_0.value = retptr; - wasm.meta(retptr, id); - const r0 = getInt32Memory0()[retptr / 4]; - const r1 = getInt32Memory0()[retptr / 4 + 1]; - const v0 = getArrayU16FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 2); - return v0; - } finally { - wasm.__wbindgen_export_0.value += 16; - } - }, - /** - * @param {number} id - * @returns {Uint8Array} - */ - buffer(id) { - try { - const retptr = wasm.__wbindgen_export_0.value - 16; - wasm.__wbindgen_export_0.value = retptr; - wasm.buffer(retptr, id); - const r0 = getInt32Memory0()[retptr / 4]; - const r1 = getInt32Memory0()[retptr / 4 + 1]; - const v0 = getArrayU8FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 1); - return v0; - } finally { - wasm.__wbindgen_export_0.value += 16; - } - }, - /** - * @param {number} id - */ - free(id) { - wasm.free(id); - } + return (wasm.decode_free(ptr), framebuffer); } \ No newline at end of file diff --git a/utils/wasm/jpeg.wasm b/utils/wasm/jpeg.wasm index ad99919..68c8cbd 100644 Binary files a/utils/wasm/jpeg.wasm and b/utils/wasm/jpeg.wasm differ diff --git a/utils/wasm/png.js b/utils/wasm/png.js new file mode 100644 index 0000000..d45d3b8 --- /dev/null +++ b/utils/wasm/png.js @@ -0,0 +1,36 @@ +import * as wasm_mod from './png.wasm'; +let ref = { deref() {} }; + +function wasm() { + let u8; + + const { + wfree, walloc, decode, memory, + width: wwidth, height: wheight, + } = wasm_mod; + + 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 }; + }, + }; +} + +export function decode(buffer) { + return (ref.deref() || (ref = new WeakRef(wasm())).deref()).decode(buffer); +} \ No newline at end of file diff --git a/utils/wasm/png.wasm b/utils/wasm/png.wasm new file mode 100755 index 0000000..b8ab267 Binary files /dev/null and b/utils/wasm/png.wasm differ diff --git a/utils/wasm/svg.js b/utils/wasm/svg.js index 6966e93..53d4f31 100644 --- a/utils/wasm/svg.js +++ b/utils/wasm/svg.js @@ -1,159 +1,33 @@ -const {readFile} = require('fs').promises; -const {join} = require('path'); - -let wasm; - -let WASM_VECTOR_LEN = 0; - -let cachegetUint8Memory0 = null; - -function getUint8Memory0() { - if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { - cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachegetUint8Memory0; +import * as wasm from './svg.wasm'; + +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); + } } -let cachedTextEncoder = new TextEncoder(); - -const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' - ? function (arg, view) { - return cachedTextEncoder.encodeInto(arg, view); - } - : function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; - }); - -function passStringToWasm0(arg, malloc, realloc) { - - if (realloc === undefined) { - const textEncoder = new TextEncoder(); - const buf = textEncoder.encode(arg); - const ptr = malloc(buf.length); - getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len); - - const mem = getUint8Memory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3); - const view = getUint8Memory0().subarray(ptr + offset, ptr + len); - const ret = encodeString(arg, view); +export function rasterize(buffer, fit, scale) { + const _w = wasm; - offset += ret.written; - } + const bptr = mem.alloc(buffer.length); + mem.u8(bptr, buffer.length).set(buffer); + const ptr = wasm.rasterize(bptr, buffer.length, fit, scale); - WASM_VECTOR_LEN = offset; - return ptr; -} - -let cachegetInt32Memory0 = null; - -function getInt32Memory0() { - if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { - cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); - } - return cachegetInt32Memory0; -} - -let cachegetUint32Memory0 = null; - -function getUint32Memory0() { - if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) { - cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer); - } - return cachegetUint32Memory0; -} - -function getArrayU32FromWasm0(ptr, len) { - return getUint32Memory0().subarray(ptr / 4, ptr / 4 + len); -} - -function getArrayU8FromWasm0(ptr, len) { - return getUint8Memory0().subarray(ptr, ptr + len); -} + if (0 === ptr) throw new Error('svg: failed to parse'); + if (1 === ptr) throw new Error('svg: failed to rasterize'); -module.exports = { - /** - * @param {number} ptr - * @param {string} svg - * @param {number} fit_kind - * @param {number} zoom - * @param {number} width - * @param {number} height - * @returns {number} - */ - async rgba(ptr, svg, fit_kind, zoom, width, height) { - if (!wasm) { - const module = new WebAssembly.Module(await readFile(join(__dirname, './svg.wasm'))); - const instance = new WebAssembly.Instance(module); - wasm = instance.exports; - } + const framebuffer = { + width: wasm.rasterize_width(ptr), + height: wasm.rasterize_height(ptr), + buffer: mem.u8(wasm.rasterize_buffer(ptr), mem.length()).slice(), + } - const ptr0 = passStringToWasm0(svg, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - return wasm.rgba(ptr, ptr0, WASM_VECTOR_LEN, fit_kind, zoom, width, height); - }, - /** - * @param {number} id - * @returns {Uint32Array} - */ - meta(id) { - try { - const retptr = wasm.__wbindgen_export_2.value - 16; - wasm.__wbindgen_export_2.value = retptr; - wasm.meta(retptr, id); - const r0 = getInt32Memory0()[retptr / 4]; - const r1 = getInt32Memory0()[retptr / 4 + 1]; - const v0 = getArrayU32FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 4); - return v0; - } finally { - wasm.__wbindgen_export_2.value += 16; - } - }, - /** - * @param {number} id - * @returns {Uint8Array} - */ - buffer(id) { - try { - const retptr = wasm.__wbindgen_export_2.value - 16; - wasm.__wbindgen_export_2.value = retptr; - wasm.buffer(retptr, id); - const r0 = getInt32Memory0()[retptr / 4]; - const r1 = getInt32Memory0()[retptr / 4 + 1]; - const v0 = getArrayU8FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1); - return v0; - } finally { - wasm.__wbindgen_export_2.value += 16; - } - }, - /** - * @param {number} id - */ - free(id) { - wasm.free(id); - } -}; \ No newline at end of file + return (wasm.rasterize_free(ptr), framebuffer); +} \ No newline at end of file diff --git a/utils/wasm/svg.wasm b/utils/wasm/svg.wasm index 988be8c..ce3a271 100644 Binary files a/utils/wasm/svg.wasm and b/utils/wasm/svg.wasm differ diff --git a/utils/wasm/tiff.js b/utils/wasm/tiff.js new file mode 100644 index 0000000..280f521 --- /dev/null +++ b/utils/wasm/tiff.js @@ -0,0 +1,31 @@ +import * as wasm from './tiff.wasm'; + +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); + } +} + +export function decode(buffer) { + const _w = wasm; + + 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); +} diff --git a/utils/wasm/tiff.wasm b/utils/wasm/tiff.wasm new file mode 100644 index 0000000..5e91acc Binary files /dev/null and b/utils/wasm/tiff.wasm differ diff --git a/utils/wasm/zlib.js b/utils/wasm/zlib.js index c763626..d71b673 100644 --- a/utils/wasm/zlib.js +++ b/utils/wasm/zlib.js @@ -1,73 +1,80 @@ -const {join} = require('path'); -const {promises: {readFile}} = require('fs'); - -async function load() { - let wasm; - - { - const module = new WebAssembly.Module(await readFile(join(__dirname, './zlib.wasm'))); - const instance = new WebAssembly.Instance(module); - - wasm = instance.exports; - } - - let u8array_ref = new Uint8Array(wasm.memory.buffer); - let i32array_ref = new Int32Array(wasm.memory.buffer); - - function u8array() { - return u8array_ref.buffer === wasm.memory.buffer ? u8array_ref : (u8array_ref = new Uint8Array(wasm.memory.buffer)); - } - - function i32array() { - return i32array_ref.buffer === wasm.memory.buffer ? i32array_ref : (i32array_ref = new Int32Array(wasm.memory.buffer)); - } - - function ptr_to_u8array(ptr, len) { - return u8array().subarray(ptr, ptr + len); - } - - function u8array_to_ptr(buffer) { - const ptr = wasm.__wbindgen_malloc(buffer.length); - u8array().set(buffer, ptr); - return ptr; - } - - return { - compress(buffer, level) { - const ptr = u8array_to_ptr(buffer); - wasm.compress(8, ptr, buffer.length, level); - - const i32 = i32array(); - const slice = ptr_to_u8array(i32[2], i32[3]).slice(); - wasm.__wbindgen_free(i32[2], i32[3]); - return slice; - }, decompress(buffer, limit) { - const ptr = u8array_to_ptr(buffer); - - try { - wasm.decompress(8, ptr, buffer.length, limit); - - const i32 = i32array(); - const slice = ptr_to_u8array(i32[2], i32[3]).slice(); - wasm.__wbindgen_free(i32[2], i32[3]); - return slice; - } catch { - wasm.__wbindgen_free(ptr, buffer.length); - throw new Error('zlib: panic'); - } - } - }; +import * as wasm from './zlib.wasm'; + +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); + } +} + +export function compress(buffer, level = 3) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + return mem.copy_and_free(wasm.compress(ptr, buffer.length, level), mem.length()); +} + +export function compress_raw(buffer, level = 3) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + return mem.copy_and_free(wasm.compress_raw(ptr, buffer.length, level), mem.length()); +} + +export function decompress(buffer, limit = 0) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + const x = wasm.decompress(ptr, buffer.length, limit); + if (0 === x) throw new Error('zlib: failed to decompress'); + + return mem.copy_and_free(x, mem.length()); +} + +export function decompress_raw(buffer, limit = 0) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + const x = wasm.decompress_raw(ptr, buffer.length, limit); + if (0 === x) throw new Error('zlib: failed to decompress (raw)'); + + return mem.copy_and_free(x, mem.length()); } -module.exports = { - async compress(buffer, level) { - const {compress} = module.exports = await load(); +export function decompress_with(buffer, limit = 0, transform) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + const x = wasm.decompress(ptr, buffer.length, limit); + if (0 === x) throw new Error('zlib: failed to decompress'); + + const u8 = mem.u8(x, mem.length()); + + const value = transform(u8); + return (mem.free(x, u8.length), value); +} + +export function decompress_raw_with(buffer, limit = 0, transform) { + const _w = wasm; + + const ptr = mem.alloc(buffer.length); + mem.u8(ptr, buffer.length).set(buffer); + const x = wasm.decompress_raw(ptr, buffer.length, limit); + if (0 === x) throw new Error('zlib: failed to decompress (raw)'); - return compress(buffer, level); - }, - async decompress(buffer, limit) { - const {decompress} = module.exports = await load(); + const u8 = mem.u8(x, mem.length()); - return decompress(buffer, limit); - } -}; \ No newline at end of file + const value = transform(u8); + return (mem.free(x, u8.length), value); +} \ No newline at end of file diff --git a/utils/wasm/zlib.wasm b/utils/wasm/zlib.wasm index 8332f68..33162ee 100644 Binary files a/utils/wasm/zlib.wasm and b/utils/wasm/zlib.wasm differ diff --git a/v2/codecs/magic.js b/v2/codecs/magic.js new file mode 100644 index 0000000..adbbdc7 --- /dev/null +++ b/v2/codecs/magic.js @@ -0,0 +1,25 @@ +const { view } = require('../util/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 new file mode 100644 index 0000000..b66a01b --- /dev/null +++ b/v2/framebuffer.d.ts @@ -0,0 +1,67 @@ +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.mjs b/v2/framebuffer.mjs new file mode 100644 index 0000000..e59efd4 --- /dev/null +++ b/v2/framebuffer.mjs @@ -0,0 +1,126 @@ +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 new file mode 100644 index 0000000..fe9684e --- /dev/null +++ b/v2/framebuffer.rs @@ -0,0 +1,825 @@ +#![allow(dead_code)] +#![warn(clippy::perf)] +#![feature(decl_macro)] +#![feature(unchecked_math)] +#![warn(clippy::complexity)] +#![feature(core_intrinsics)] +#![warn(clippy::correctness)] +#![allow(non_camel_case_types)] +#![allow(non_upper_case_globals)] +#![feature(destructuring_assignment)] +#![feature(const_fn_floating_point_arithmetic)] + +#[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 {} + +impl Drop for framebuffer { + fn drop(&mut self) { if self.ptr.0 { free(self.ptr_mut(), self.len()); } } +} + +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 { + pub fn new(width: usize, height: usize) -> Self { return Self { width, height, ptr: (true, calloc(4 * width * height)) }; } + pub const unsafe fn from_ptr(width: usize, height: usize, ptr: *mut u8) -> Self { return Self { width, height, ptr: (false, ptr) }; } + pub unsafe fn new_fast(width: usize, height: usize) -> Self { return Self { width, height, ptr: (true, alloc(4 * width * height)) }; } + pub const unsafe fn from(width: usize, height: usize, buffer: &[u8]) -> Self { return Self { width, height, ptr: (false, buffer.as_ptr() as _) }; } + pub unsafe fn from_mut(width: usize, height: usize, buffer: &mut [u8]) -> Self { return Self { width, height, ptr: (false, buffer.as_mut_ptr()) }; } + + pub fn ptr_mut(&mut self) -> *mut T { return self.ptr.1 as *mut T; } + pub const fn ptr(&self) -> *const T { return self.ptr.1 as *const T; } + pub const fn len(&self) -> usize { return 4 * self.width * self.height; } + pub fn slice(&self) -> &[T] { return unsafe { std::slice::from_raw_parts(self.ptr(), self.len() / std::mem::size_of::()) }; } + pub fn set(&mut self, x: usize, y: usize, color: colors::rgba) { unsafe { *self.ptr_mut::().add(x + y * self.width) = color.into() }; } + pub fn get(&self, x: usize, y: usize) -> colors::rgba { return unsafe { std::mem::transmute(*self.ptr::().add(x + y * self.width)) }; } + pub fn slice_mut(&mut self) -> &mut [T] { return unsafe { std::slice::from_raw_parts_mut(self.ptr_mut(), self.len() / std::mem::size_of::()) }; } + pub fn vec(mut self) -> Vec { self.ptr.0 = false; return unsafe { Vec::from_raw_parts(self.ptr.1 as *mut T, self.len() / std::mem::size_of::(), alloc_align(self.len()).size() / std::mem::size_of::()) }; } +} + +pub mod colors { + #[repr(C)] #[derive(Copy, Clone)] pub struct rgb { pub r: u8, pub g: u8, pub b: u8 } + #[repr(C)] #[derive(Copy, Clone)] 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.ptr_mut::().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.ptr_mut::(); + + 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.ptr_mut::(); + 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.ptr_mut::(); + + 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.ptr_mut::(); + 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.ptr_mut::(); + 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.ptr_mut::(); + 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, 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.ptr_mut::(); + 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.ptr_mut::(); + 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.ptr_mut::(); + 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.ptr_mut::(); + + 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 fn function(fb: &mut fb, f: F) where F: Fn(usize, usize) -> u32 { + let width = fb.width; + let height = fb.height; + let u32 = fb.ptr_mut::(); + + for y in 0..height { + let yoffset = y * width; + for x in 0..width { unsafe { *u32.add(x + yoffset) = f(x, y); } } + } + } + + pub fn color(fb: &mut fb, color: colors::rgba) { + if color.r == color.g && color.r == color.b && color.r == color.a { unsafe { fb.ptr_mut::().write_bytes(color.r, fb.len()); } } + + else { + let width = fb.width; + let height = fb.height; + let u32 = fb.ptr_mut::(); + 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.ptr_mut::(); + + 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.slice_mut::().reverse(); + } + + pub fn rotate90(fb: &mut fb) { + let width = fb.width; + let height = fb.height; + let u32 = fb.ptr_mut::(); + 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.ptr_mut::(); + 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.ptr::(); + let b32 = fb.ptr_mut::(); + 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.ptr::(); + let b32 = fb.ptr_mut::(); + 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.ptr::(); + let b32 = fb.ptr_mut::(); + 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.ptr::(); + let mut fb = framebuffer::new(width, height); + + let u32 = fb.ptr_mut::(); + 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.ptr::(); + let mut fb = framebuffer::new(width, height); + + let mut offset = 0; + let u8 = fb.ptr_mut::(); + 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.ptr::(); + let mut fb = framebuffer::new(width, height); + + let mut offset = 0; + let u8 = fb.ptr_mut::(); + 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.ptr_mut::(); + 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); + } + } + } + } +} \ No newline at end of file diff --git a/v2/ops/blur.mjs b/v2/ops/blur.mjs new file mode 100644 index 0000000..86ba29d --- /dev/null +++ b/v2/ops/blur.mjs @@ -0,0 +1,303 @@ +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 new file mode 100644 index 0000000..b528aac --- /dev/null +++ b/v2/ops/color.mjs @@ -0,0 +1,277 @@ +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 new file mode 100644 index 0000000..261f529 --- /dev/null +++ b/v2/ops/crop.mjs @@ -0,0 +1,64 @@ +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 new file mode 100644 index 0000000..0743c25 --- /dev/null +++ b/v2/ops/fill.mjs @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..6d0902c --- /dev/null +++ b/v2/ops/flip.mjs @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..dcd9bd6 --- /dev/null +++ b/v2/ops/index.mjs @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..bd460aa --- /dev/null +++ b/v2/ops/iterator.mjs @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..b0184b1 --- /dev/null +++ b/v2/ops/overlay.mjs @@ -0,0 +1,63 @@ +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 new file mode 100644 index 0000000..a822037 --- /dev/null +++ b/v2/ops/resize.mjs @@ -0,0 +1,166 @@ +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 new file mode 100644 index 0000000..627ad61 --- /dev/null +++ b/v2/ops/rotate.mjs @@ -0,0 +1,117 @@ +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 new file mode 100644 index 0000000..ce6114e --- /dev/null +++ b/v2/util/mem.mjs @@ -0,0 +1,24 @@ +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