UNPKG

@oazmi/kitchensink

Version:

a collection of personal utility functions

278 lines 16.4 kB
/** utility functions for handling images along with canvas tools. * * TODO: add test code within documentation. however, I'm uncertain how I would go about testing canvas/offscreencanvas features within Deno. * * @module */ import "./_dnt.polyfills.js"; import { type Rect, type SimpleImageData } from "./struct.js"; import type { Optional } from "./typedefs.js"; /** an image source acceptable by a {@link constructImageBitmapSource}, which generates an `ImageBitmapSource` for a `Canvas` or an `OffscreenCanvas`. * * if the source is a `string`, then it must be URI. i.e. data-uri, or url-links or relative-links are all acceptable, * so long as they can be assigned to `HTMLImageElement.src`. */ export type AnyImageSource = string | Uint8Array | Uint8ClampedArray | ArrayBuffer | Array<number> | ImageBitmapSource; /** image source mime-types accepted by most modern web-browsers. */ export type ImageMIMEType = `image/${"gif" | "jpeg" | "jpg" | "png" | "svg+xml" | "webp"}`; /** a base64-encoded image-data uri's header component. */ export type Base64ImageHeader = `data:${ImageMIMEType};base64,`; /** an image-data uri (in base64 encoding). */ export type Base64ImageString = `${Base64ImageHeader}${string}`; /** an blob containing image data. */ export type ImageBlob = Blob & { type: ImageMIMEType; }; /** get the global background `OffscreenCanvas`. * * if the offscreen-canvas has not been initialized prior, it will be initialized with your provided optional `init_width` and `init_height` dimensions. * however, if it _has_ been already initialized, then it **will not** resize to your provided size. * thus, if you want a specific size, you should **always** set it manually yourself. * * the reason why we don't resize here, is because resizing clears the canvas, and thus, you will lose your image if you were planning to read it back. */ export declare const getBgCanvas: (init_width?: number | undefined, init_height?: number | undefined) => OffscreenCanvas; /** get the global background `OffscreenCanvas`'s 2d-rendering context, and make it specialize in frequent reading (i.e. `willReadFrequently` is set to `true`). * * if the offscreen-canvas has not been initialized prior, it will be initialized with your provided optional `init_width` and `init_height` dimensions. * however, if it _has_ been already initialized, then it **will not** resize to your provided size. * thus, if you want a specific size, you should **always** set it manually yourself. * * the reason why we don't resize here, is because resizing clears the canvas, and thus, you will lose your image if you were planning to read it back. */ export declare const getBgCtx: (init_width?: number | undefined, init_height?: number | undefined) => OffscreenCanvasRenderingContext2D; /** check of the provided string is a base64 string, by simply observing if it starts with `"data:image/"`. */ export declare const isBase64Image: (str?: string) => str is Base64ImageString; /** get the header of a base64 image. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * img_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAD...", * img_header = getBase64ImageHeader(img_uri) * * assertEquals(img_header, "data:image/png;base64,") * ``` */ export declare const getBase64ImageHeader: (str: Base64ImageString) => Base64ImageHeader; /** get the mime type of a base64 image. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * img_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAD...", * img_mime = getBase64ImageMIMEType(img_uri) * * assertEquals(img_mime, "image/png") * ``` */ export declare const getBase64ImageMIMEType: (str: Base64ImageString) => ImageMIMEType; /** get the body data portion of a base64 image. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * img_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAD...", * img_body = getBase64ImageBody(img_uri) * * assertEquals(img_body, "iVBORw0KGgoAAAANSUhEUgAAD...") * ``` */ export declare const getBase64ImageBody: (str: Base64ImageString) => string; /** load an image as a `Blob`, with the chosen optional `type` encoding (default is "image/png"). * * possible image sources are: * - data uri for base64 encoded image `string` * - http url path `string` * - file url path `string` * - buffer of RGBA pixel byte data as `Uint8Array`, `Uint8ClampedArray`, `ArrayBuffer`, or `Array<number>` * - `ImageBitmapSource`, which includes: * - `Blob` * - `ImageData` * - `HTMLCanvasElement` or `OffscreenCanvas` * - `HTMLImageElement` or `SVGImageElement` or `HTMLVideoElement` * - `ImageBitmap` * * @param img_src anything image representation that can be constructed into an `ImageBitmap` using {@link constructImageBitmapSource} * @param width when using `Uint8Array`, `Uint8ClampedArray`, `ArrayBuffer`, or `Array<number>` as `img_src`, you must necessarily provide the width of the image * @param crop_rect specify a cropping rectangle * @param bitmap_options these are {@link ImageBitmapOptions} that can be used for optionally cropping the `img_src`, changing its colorSpace, etc... * @param blob_options specify `type`: {@link ImageMIMEType} and `quality`: number to encode your output Blob into * * > [!note] * > note that when using `Uint8Array`, `Uint8ClampedArray`, `ArrayBuffer`, or `Array<number>`, you should provide a `width` as the second argument to this function. * > you can also provide an optional image element as the third argument to load the given `img_src` onto, otherwise a new one will be made. */ export declare const constructImageBlob: (img_src: AnyImageSource, width?: number, crop_rect?: Rect, bitmap_options?: ImageBitmapOptions, blob_options?: Parameters<OffscreenCanvas["convertToBlob"]>[0]) => Promise<ImageBlob>; /** extract the {@link ImageData} from an image source (of type {@link CanvasImageSource}), with optional cropping. * * due to the fact that this function utilizes a `Canvas`/`OffscreenCanvas`, * it is important to note that the output `ImageData` is sometimes lossy in nature. * this is because gpu-accelerated web-browsers _approximate_ the colors (i.e. you don't truly get `256^3` unique colors), * and also due to rounding errors from/to internal float-valued colors and output integer-valued colors. * * but generally speaking, the `ImageData` can be lossless if all of the following are satisfied: * - disable gpu-acceleration of your web-browser, through the `flags` page (`"chrome://flags"` or `"about:config"`). * - your `img_src` has either no alpha-channel, or 100% visible alpha-channel throughout (i.e. non-transparent image). * - you have pre-multiplied alpha disabled (TODO: this part can be achieved/implemented by this library, but I haven't looked into it yet.). * check out this [script](https://github.com/backspaces/agentscript/blob/master/src/RGBADataSet.js#L27) * for using webgl for reading and writing bitmap pixels without losing color accuracy due to alpha premultiplication * * @param img_src an image source can be an `HTMLImageElement`, `HTMLCanvasElement`, `ImageBitmap`, etc.. * @param crop_rect dimension of the cropping rectangle. leave as `undefined` if you wish not to crop, or only provide a partial {@link Rect} */ export declare const constructImageData: (img_src: AnyImageSource, width?: number, crop_rect?: Rect, bitmap_options?: ImageBitmapOptions, image_data_options?: ImageDataSettings) => Promise<ImageData>; /** conveniently construct an `ImageBitmapSource` from different image source types. * * if you use a raw buffer (`number[] | Uint8Array | Uint8ClampedArray | ArrayBuffer`) as the image source, * then you are expected to provide the `width` of the image. */ export declare const constructImageBitmapSource: (img_src: AnyImageSource, width?: number) => Promise<ImageBitmapSource>; /** get a grayscale intensity bitmap of multi-channel `pixel_buf` image buffer, * using an optional `alpha_channel` number to mask off the resulting intensity wherever the masking channel is zero * (or less than the optional `alpha_bias` parameter). * * @param pixels_buf flattened pixel bytes * @param channels number of color channels (ie: bytes per pixel). for instance, you'd use `4` for RGBA, `3` for RGB, `1` for L, `2` for AL, etc... * @param alpha_channel specify which channel is the alpha channel, or leave it as `undefined` to dictate lack of thereof. for instance, you'd use `3` for RGB**A**, `0` for **A**L, and `undefined` for RGB * @param alpha_bias if alpha is present, you can specify the minimum alpha value required for the pixel to be visible. anything less will make the pixel dull */ export declare const intensityBitmap: (pixels_buf: Uint8Array | Uint8ClampedArray, channels: number, alpha_channel?: number | undefined, alpha_bias?: number) => Uint8Array; type PaddingCondition = { 1: (v: number) => number; 2: (l: number, a: number) => number; 3: (r: number, g: number, b: number) => number; 4: (r: number, g: number, b: number, a: number) => number; }; /** get the bounding box {@link Rect} of an image, based on an accumulative `padding_condition` function that should return * `0.0` for padding/whitespace/empty pixels, and positive numbers (usually `1.0`) for non-padding/important pixels. * * - if the summation of `padding_condition` applied onto a particular row, or column of pixels is less than `minimum_non_padding_value`, * then that entire row/column will be counted as an empty padding space. * - else, if the summation of `padding_condition` is greater than `minimum_non_padding_value`, * then that specific row/column will be counted as one of the bounding box's sides. * - take a look at {@link trimImagePadding} to get an understanding of a potential use case. * * you do not need to specify the number of channels in your `img_data`, * because it will be calculated automatically via `img_data.width`, `img_data.height`, and `img_data.data.length`. * * ### A note on performance * * almost all performance depends purely on the complexity of your `padding_condition`. * - if the equations in `padding_condition` uses square-roots, exponents, and if-conditions, then expect a major performance drop. * - if your equations consist only of only numeric operations `+, -, *, /`, then the performance will be much faster. * * some unsuccessful benchmarks I've tried to boost the performance (in V8): * - defining `rowAt`, `colAt`, and `nonPaddingValue` outside of this function, instead of inlining them, made no difference in the performance. * - substituting `padding_condition` in `nonPaddingValue` with the actual arithmetic functions via inlining * (and thereby avoiding repeated function calls) makes no difference, thanks to the JIT doing the inlining on its own in V8. * - finally, the `colAt` inline function is surprisingly super fast (almost as fast as `rowAt`). * so, bounding top and bottom is not at all noticeably quicker than bounding left and right. */ export declare const getBoundingBox: <Channels extends (1 | 2 | 3 | 4) = 4>(img_data: SimpleImageData, padding_condition: PaddingCondition[Channels], minimum_non_padding_value?: number) => Rect; /** crop an {@link ImageData} or arbitrary channel {@link SimpleImageData} with the provided `crop_rect`. * * the original `img_data` is not mutated, and the returned cropped image data contains data that has been copied over. */ export declare const cropImageData: <Channels extends (1 | 2 | 3 | 4) = 4>(img_data: SimpleImageData, crop_rect: Partial<Rect>) => SimpleImageData; /** trim the padding of an image based on sum of pixel conditioning of each border's rows and columns. * * > [!note] * > the output will always consist of at least 1-pixel width or 1-pixel height. * * @example * for example, to trim the whitespace border pixels of an "RGBA" image, irrespective of the alpha, * and a minimum requirement of at least three non-near-white pixels in each border row and column, * you would design your `padding_condition` as such: * * ```ts * import { assertEquals } from "jsr:@std/assert" * * // we want the distance between a pixel's rgb color and the white color (i.e. `(255, 255, 255,)`) * // to be less than `10 * Math.sqrt(3)`, in order for it to be considered near-white. * const my_white_padding_condition = (r: number, g: number, b: number, a: number) => { * const distance_from_white = (3 * 255**2) - (r**2 + g**2 + b**2) * return distance_from_white < (3 * 5**2) * ? 0.0 * : 1.0 * } * * const * width = 60, * img_data = new ImageData(new Uint8ClampedArray(4 * width * 30).fill(255), width), // fully white rgba image * trimmed_img_data = trimImagePadding(img_data, my_white_padding_condition, 3.0) // only a 1x1 white pixel remains * * assertEquals(trimmed_img_data.width, 1) * assertEquals(trimmed_img_data.height, 1) * assertEquals(trimmed_img_data.data, new Uint8ClampedArray(4).fill(255)) * ``` */ export declare const trimImagePadding: <Channels extends (1 | 2 | 3 | 4)>(img_data: SimpleImageData, padding_condition: PaddingCondition[Channels], minimum_non_padding_value?: number) => SimpleImageData; /** a utility type used by {@link coordinateTransformer}, for specifying an image's coordinate space (size dimensions + number of channels). */ export interface ImageCoordSpace extends Rect { channels: (1 | 2 | 3 | 4); } /** get a function that maps index-coordinates of image0-coordinate-space to the index-coordinates of image1-coordinate-space. * * note that if you're mapping lots of indexes using `Array.map`, it would be nearly 40-times faster to use the {@link lambdacalc.vectorize1} function instead. * * @param `coords0` object defining the first ImageCoordSpace * @param `coords1` object defining the second ImageCoordSpace * @returns `(i0: number & coord0) => i1 as number & coord1` a function that takes in an integer index from coordinate space coords0 and converts it so that it's relative to coordinate space coords1 * * @example * ```ts * // suppose you have an RGBA image data buffer of `width = 100`, `height = 50`, `channels = 4`, * // and you have an array of 6 pixel indexes: `idx0 = [1040, 1044, 1048, 1140, 1144, 1148]`, * // and now, you want to convert these indexes to that of an "LA" image data buffer of: * // `width = 10`, `height = 10`, and `channels = 2`, `x = 5`, `y = 10` * // then: * const * coords0 = {x: 0, y: 0, width: 100, height: 50, channels: 4 as const}, * coords1 = {x: 5, y: 10, width: 10, height: 10, channels: 2 as const}, * coords0_to_coords1 = coordinateTransformer(coords0, coords1), * idx0 = [4040, 4044, 4048, 4440, 4444, 4448], * idx1 = idx0.map(coords0_to_coords1) // [10, 12, 14, 30, 32, 34] * ``` * * ### Derivation * * the equation for `mask_intervals` can be easily derived as follows: * - `p0 = px of data`, `y0 = y-coords of pixel in data`, `x0 = x-coords of pixel in data`, `w0 = width of data`, `c0 = channels of data` * - `p1 = px of mask`, `y1 = y-coords of pixel in mask`, `x1 = x-coords of pixel in mask`, `w1 = width of mask`, `c1 = channels of mask` * - `y = y-coords of mask's rect`, `x = x-coords of mask's rect` * * ```ts ignore * declare let [w0, w1, c0, c1]: number[] * let * p0 = (x0 + y0 * w0) * c0, * x0 = (p0 / c0) % w0, * y0 = trunc(p0 / (c0 * w0)), * p1 = (x1 + y1 * w1) * c1, * x1 = (p1 / c1) % w1, * y1 = trunc(p1 / (c1 * w1)), * x = x0 - x1, * y = y0 - y1 * * // so, now: * p1 = c1 * (x1 + y1 * w1) * p1 = c1 * ((x0 - x) + (y0 - y) * w1) * p1 = c1 * ((((p0 / c0) % w0) - x) + (((p0 / c0) / w0 | 0) - y) * w1) * ``` */ export declare const coordinateTransformer: (coords0: Optional<ImageCoordSpace, "height" | "x" | "y">, coords1: Optional<ImageCoordSpace, "height" | "x" | "y">) => ((i0: number) => number); /** TODO: implement the darn thing. * * EDIT: I don't even recall what this function was meant to do. * was it made to generate random rgba pixel color that can be assigned to the 2d-rendering context? * moreover, did I not want to permutate over a set number of colors instead of generating any random 256**3 color (i.e. less distinguishable)? */ export declare const randomRGBA: (alpha?: undefined | number) => void; export {}; //# sourceMappingURL=image.d.ts.map