UNPKG

@napi-rs/image

Version:

Image processing library

690 lines (554 loc) 20.4 kB
# `@napi-rs/image` Transform and optimize images library. See [Examples](../../example.mjs) for usage. [![install size](https://packagephobia.com/badge?p=@napi-rs/image)](https://packagephobia.com/result?p=@napi-rs/image) [![Downloads](https://img.shields.io/npm/dm/@napi-rs/image.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/image?minimal=true) ## Transformer: This library support encode/decode these formats: | Format | Input | Output | | --------- | ----------------------------------------- | --------------------------------------- | | RawPixels | RGBA 8 bits pixels | | | JPEG | Baseline and progressive | Baseline JPEG | | PNG | All supported color types | Same as decoding | | BMP | | Rgb8, Rgba8, Gray8, GrayA8 | | ICO | | | | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 | | WebP | | | | AVIF | | | | PNM | PBM, PGM, PPM, standard PAM | | | DDS | DXT1, DXT3, DXT5 | No | | TGA | | Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayA8 | | OpenEXR | Rgb32F, Rgba32F (no dwa compression) | Rgb32F, Rgba32F (no dwa compression) | | farbfeld | | | See [index.d.ts](./index.d.ts) for API reference. ### New from Constructor ```ts import { Transformer } from '@napi-rs/image' const transformer = new Transformer(input) ``` ### New from RGBA RawPixels ```ts import { Transformer } from '@napi-rs/image' import { decode } from 'blurhash' // Uint8ClampedArray const pixels = decode('LEHV6nWB2yk8pyo0adR*.7kCMdnj', 32, 32) const transformer = Transformer.fromRgbaPixels(pixels, 32, 32) ``` ### Metadata ```ts metadata(withExif?: boolean | undefined | null, signal?: AbortSignal | undefined | null): Promise<Metadata> export interface Metadata { width: number height: number exif?: Record<string, string> | undefined | null orientation?: number | undefined | null format: string colorType: JsColorType } export enum JsColorType { /** Pixel is 8-bit luminance */ L8 = 0, /** Pixel is 8-bit luminance with an alpha channel */ La8 = 1, /** Pixel contains 8-bit R, G and B channels */ Rgb8 = 2, /** Pixel is 8-bit RGB with an alpha channel */ Rgba8 = 3, /** Pixel is 16-bit luminance */ L16 = 4, /** Pixel is 16-bit luminance with an alpha channel */ La16 = 5, /** Pixel is 16-bit RGB */ Rgb16 = 6, /** Pixel is 16-bit RGBA */ Rgba16 = 7, /** Pixel is 32-bit float RGB */ Rgb32F = 8, /** Pixel is 32-bit float RGBA */ Rgba32F = 9 } ``` **Example**: ```ts import { promises as fs } from 'fs' import { Transformer } from '@napi-rs/image' const WITH_EXIF_JPG = await fs.readFile('with-exif.jpg') const decoder = new Transformer(WITH_EXIF_JPG) const metadata = await decoder.metadata(true) ``` The metadata will be ```js { colorType: 2, exif: { Orientation: 'Unknown (5)', 'Resolution Unit': 'in', 'This image has an Exif SubIFD': '90', 'X Resolution': '72 pixels per in', 'Y Resolution': '72 pixels per in', }, format: 'jpeg', height: 450, orientation: 5, width: 600, } ``` ### Transform Image format ```ts import { promises as fs } from 'fs' import { Transformer } from '@napi-rs/image' const PNG = await fs.readFile('./un-optimized.png') const webp = await new Transformer(PNG).webp() await fs.writeFile('optimized.webp) ``` #### webp > The quality factor `quality_factor` ranges from 0 to 100 and controls the loss and quality during compression. > > The value 0 corresponds to low quality and small output sizes, whereas 100 is the highest quality and largest output size. > > https://developers.google.com/speed/webp/docs/api#simple_encoding_api > > Default is 90 ```ts webp(qualityFactor: number, signal?: AbortSignal | undefined | null): Promise<Buffer> webpSync(qualityFactor: number): Buffer /// Encode lossless webp image webpLossless(signal?: AbortSignal | undefined | null): Promise<Buffer> webpLosslessSync(): Buffer ``` #### AVIF **Config**: ```ts export interface AvifConfig { /** 0-100 scale 100 is lossless */ quality?: number | undefined | null /** 0-100 scale */ alphaQuality?: number | undefined | null /** rav1e preset 1 (slow) 10 (fast but crappy), default is 4 */ speed?: number | undefined | null /** How many threads should be used (0 = match core count) */ threads?: number | undefined | null /** set to '4:2:0' to use chroma subsampling, default '4:4:4' */ chromaSubsampling?: ChromaSubsampling | undefined | null } /** * https://en.wikipedia.org/wiki/Chroma_subsampling#Types_of_sampling_and_subsampling * https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts */ export enum ChromaSubsampling { /** * Each of the three Y'CbCr components has the same sample rate, thus there is no chroma subsampling. This scheme is sometimes used in high-end film scanners and cinematic post-production. * Note that "4:4:4" may instead be wrongly referring to R'G'B' color space, which implicitly also does not have any chroma subsampling (except in JPEG R'G'B' can be subsampled). * Formats such as HDCAM SR can record 4:4:4 R'G'B' over dual-link HD-SDI. */ Yuv444 = 0, /** * The two chroma components are sampled at half the horizontal sample rate of luma: the horizontal chroma resolution is halved. This reduces the bandwidth of an uncompressed video signal by one-third. * Many high-end digital video formats and interfaces use this scheme: * - [AVC-Intra 100](https://en.wikipedia.org/wiki/AVC-Intra) * - [Digital Betacam](https://en.wikipedia.org/wiki/Betacam#Digital_Betacam) * - [Betacam SX](https://en.wikipedia.org/wiki/Betacam#Betacam_SX) * - [DVCPRO50](https://en.wikipedia.org/wiki/DV#DVCPRO) and [DVCPRO HD](https://en.wikipedia.org/wiki/DV#DVCPRO_HD) * - [Digital-S](https://en.wikipedia.org/wiki/Digital-S) * - [CCIR 601](https://en.wikipedia.org/wiki/Rec._601) / [Serial Digital Interface](https://en.wikipedia.org/wiki/Serial_digital_interface) / [D1](https://en.wikipedia.org/wiki/D-1_(Sony)) * - [ProRes (HQ, 422, LT, and Proxy)](https://en.wikipedia.org/wiki/Apple_ProRes) * - [XDCAM HD422](https://en.wikipedia.org/wiki/XDCAM) * - [Canon MXF HD422](https://en.wikipedia.org/wiki/Canon_XF-300) */ Yuv422 = 1, /** * n 4:2:0, the horizontal sampling is doubled compared to 4:1:1, * but as the **Cb** and **Cr** channels are only sampled on each alternate line in this scheme, the vertical resolution is halved. * The data rate is thus the same. * This fits reasonably well with the PAL color encoding system, since this has only half the vertical chrominance resolution of [NTSC](https://en.wikipedia.org/wiki/NTSC). * It would also fit extremely well with the [SECAM](https://en.wikipedia.org/wiki/SECAM) color encoding system, * since like that format, 4:2:0 only stores and transmits one color channel per line (the other channel being recovered from the previous line). * However, little equipment has actually been produced that outputs a SECAM analogue video signal. * In general, SECAM territories either have to use a PAL-capable display or a [transcoder](https://en.wikipedia.org/wiki/Transcoding) to convert the PAL signal to SECAM for display. */ Yuv420 = 2, /** * What if the chroma subsampling model is 4:0:0? * That says to use every pixel of luma data, but that each row has 0 chroma samples applied to it. The resulting image, then, is comprised solely of the luminance data—a greyscale image. */ Yuv400 = 3, } ``` ```ts avif(options?: AvifConfig | undefined | null, signal?: AbortSignal | undefined | null): Promise<Buffer> avifSync(options?: AvifConfig | undefined | null): Buffer ``` #### PNG **PngEncodeOptions**: ```ts export interface PngEncodeOptions { /** Default is `CompressionType::Default` */ compressionType?: CompressionType | undefined | null /** Default is `FilterType::NoFilter` */ filterType?: FilterType | undefined | null } export enum CompressionType { /** Default compression level */ Default = 0, /** Fast, minimal compression */ Fast = 1, /** High compression level */ Best = 2, /** Huffman coding compression */ Huffman = 3, /** Run-length encoding compression */ Rle = 4, } export enum FilterType { /** * No processing done, best used for low bit depth greyscale or data with a * low color count */ NoFilter = 0, /** Filters based on previous pixel in the same scanline */ Sub = 1, /** Filters based on the scanline above */ Up = 2, /** Filters based on the average of left and right neighbor pixels */ Avg = 3, /** Algorithm that takes into account the left, upper left, and above pixels */ Paeth = 4, /** * Uses a heuristic to select one of the preceding filters for each * scanline rather than one filter for the entire image */ Adaptive = 5, } ``` ```ts png(options?: PngEncodeOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Buffer> pngSync(options?: PngEncodeOptions | undefined | null): Buffer ``` #### JPEG ```ts /** default `quality` is 90 */ jpeg(quality?: number | undefined | null, signal?: AbortSignal | undefined | null): Promise<Buffer> /** default `quality` is 90 */ jpegSync(quality?: number | undefined | null): Buffer ``` #### BMP ```ts bmp(signal?: AbortSignal | undefined | null): Promise<Buffer> bmpSync(): Buffer ``` #### ICO ```ts ico(signal?: AbortSignal | undefined | null): Promise<Buffer> icoSync(): Buffer ``` #### TIFF ```ts tiff(signal?: AbortSignal | undefined | null): Promise<Buffer> tiffSync(): Buffer ``` #### PNM ```ts pnm(signal?: AbortSignal | undefined | null): Promise<Buffer> pnmSync(): Buffer ``` #### TGA ```ts tga(signal?: AbortSignal | undefined | null): Promise<Buffer> tgaSync(): Buffer ``` #### Farbfeld ```ts farbfeld(signal?: AbortSignal | undefined | null): Promise<Buffer> farbfeldSync(): Buffer ``` ### Manipulate Image #### `rotate` > Rotate the image with exif orientation, if the input image contains no exif information, this API will have no effect. ```ts /** * Rotate with exif orientation * If the orientation param is not null, * the new orientation value will override the exif orientation value */ rotate(): this ``` **Example**: This image has orientation value `5` in exif: <img src="../../with-exif.jpg" alt="with-exif.jpg" width="200" /> Without rotate: ```ts import { promises as fs } from 'fs' import { Transformer } from '@napi-rs/image' const WITH_EXIF_JPG = await fs.readFile('with-exif.jpg') const imageOutputWithoutRotateWebp = await new Transformer(WITH_EXIF).resize(450 / 2).webp(75) writeFileSync('output-exif.no-rotate.image.webp', imageOutputWithoutRotateWebp) ``` <img src="../../output-exif.no-rotate.image.webp" alt="output-exif.no-rotate.image.webp" width="200" /> With rotate: ```ts import { promises as fs } from 'fs' import { Transformer } from '@napi-rs/image' const WITH_EXIF_JPG = await fs.readFile('with-exif.jpg') const imageOutputWebp = await new Transformer(WITH_EXIF) .rotate() .resize(450 / 2) .webp(75) console.timeEnd('@napi-rs/image webp') writeFileSync('output-exif.image.webp', imageOutputWebp) ``` <img src="../../output-exif.image.webp" alt="output-exif.image.webp" width="200" /> #### `grayscale` ```ts /** * Return a grayscale version of this image. * Returns `Luma` images in most cases. However, for `f32` images, * this will return a greyscale `Rgb/Rgba` image instead. */ grayscale(): this ``` #### `invert` > Invert the colors of this image. ```ts invert(): this ``` #### `resize` ```ts /** * Resize this image using the specified filter algorithm. * The image is scaled to the maximum possible size that fits * within the bounds specified by `width` and `height`. */ resize(width: number, height?: number | undefined | null, filterType?: ResizeFilterType | undefined | null): this export enum ResizeFilterType { /** Nearest Neighbor */ Nearest = 0, /** Linear Filter */ Triangle = 1, /** Cubic Filter */ CatmullRom = 2, /** Gaussian Filter */ Gaussian = 3, /** Lanczos with window 3 */ Lanczos3 = 4 } ``` #### `overlay` ```ts /** * Overlay an image at a given coordinate (x, y) */ overlay(onTop: Buffer, x: number, y: number): this ``` ```ts import { writeFileSync } from 'fs' import { Transformer } from '@napi-rs/image' const imageOutputPng = await new Transformer(PNG).overlay(PNG, 200, 200).png() writeFileSync('output-overlay-png.png', imageOutputPng) ``` **ResizeFilterType**: To test the different sampling filters on a real example, you can find two examples called [`scaledown`](https://github.com/image-rs/image/tree/master/examples/scaledown) and [`scaleup`](https://github.com/image-rs/image/tree/master/examples/scaleup) in the `examples` directory of the crate source code. Here is a 3.58 MiB [test image](https://github.com/image-rs/image/blob/master/examples/scaledown/test.jpg) that has been scaled down to 300x225 px: <!-- NOTE: To test new test images locally, replace the GitHub path with `../../../docs/` --> <div style="display: flex; flex-wrap: wrap; align-items: flex-start;"> <div style="margin: 0 8px 8px 0;"> <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-near.png" title="Nearest"><br> Nearest Neighbor </div> <div style="margin: 0 8px 8px 0;"> <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-tri.png" title="Triangle"><br> Linear: Triangle </div> <div style="margin: 0 8px 8px 0;"> <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-cmr.png" title="CatmullRom"><br> Cubic: Catmull-Rom </div> <div style="margin: 0 8px 8px 0;"> <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-gauss.png" title="Gaussian"><br> Gaussian </div> <div style="margin: 0 8px 8px 0;"> <img src="https://raw.githubusercontent.com/image-rs/image/master/examples/scaledown/scaledown-test-lcz2.png" title="Lanczos3"><br> Lanczos with window 3 </div> </div> **Speed** Time required to create each of the examples above, tested on an Intel i7-4770 CPU with Rust 1.37 in release mode: <table style="width: auto;"> <tr> <th>Nearest</th> <td>31 ms</td> </tr> <tr> <th>Triangle</th> <td>414 ms</td> </tr> <tr> <th>CatmullRom</th> <td>817 ms</td> </tr> <tr> <th>Gaussian</th> <td>1180 ms</td> </tr> <tr> <th>Lanczos3</th> <td>1170 ms</td> </tr> </table> #### `blur` > Performs a Gaussian blur on this image. <br/> > sigma` is a measure of how much to blur by. ```ts blur(sigma: number): this ``` #### `unsharpen` > Performs an unsharpen mask on this image. <br/> `sigma` is the amount to blur the image by. <br/> `threshold` is a control of how much to sharpen. > > See <https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking> ```ts unsharpen(sigma: number, threshold: number): this ``` #### `filter3x3` Filters this image with the specified 3x3 kernel. Error will thrown if the kernel length is not `9`. ```ts filter3x3(kernel: Array<number>): this ``` #### `adjustContrast` > Adjust the contrast of this image.<br/> `contrast` is the amount to adjust the contrast by.<br/> > Negative values decrease the contrast and positive values increase the contrast. ```ts adjustContrast(contrast: number): this ``` #### `brighten` > Brighten the pixels of this image.<br/> `value` is the amount to brighten each pixel by. <br/> > Negative values decrease the brightness and positive values increase it. ```ts brighten(brightness: number): this ``` #### `huerotate` > Hue rotate the supplied image.<br/> `value` is the degrees to rotate each pixel by. > 0 and 360 do nothing, the rest rotates by the given degree value. > just like the css webkit filter hue-rotate(180) ```ts huerotate(hue: number): this ``` ## Optimize PNG ### Lossless compression Lossless optimize PNG powered by [oxipng](https://github.com/shssoichiro/oxipng). **PNGLosslessOptions** ```ts export interface PNGLosslessOptions { /** * Attempt to fix errors when decoding the input file rather than returning an Err. * Default: `false` */ fixErrors?: boolean | undefined | null /** * Write to output even if there was no improvement in compression. * Default: `false` */ force?: boolean | undefined | null /** Which filters to try on the file (0-5) */ filter?: Array<number> | undefined | null /** * Whether to attempt bit depth reduction * Default: `true` */ bitDepthReduction?: boolean | undefined | null /** * Whether to attempt color type reduction * Default: `true` */ colorTypeReduction?: boolean | undefined | null /** * Whether to attempt palette reduction * Default: `true` */ paletteReduction?: boolean | undefined | null /** * Whether to attempt grayscale reduction * Default: `true` */ grayscaleReduction?: boolean | undefined | null /** * Whether to perform IDAT recoding * If any type of reduction is performed, IDAT recoding will be performed regardless of this setting * Default: `true` */ idatRecoding?: boolean | undefined | null /** Whether to remove ***All non-critical headers*** on PNG */ strip?: boolean | undefined | null /** Whether to use heuristics to pick the best filter and compression */ useHeuristics?: boolean | undefined | null } ``` ```ts export function losslessCompressPng( input: Buffer, options?: PNGLosslessOptions | undefined | null, signal?: AbortSignal | undefined | null, ): Promise<Buffer> export function losslessCompressPngSync(input: Buffer, options?: PNGLosslessOptions | undefined | null): Buffer ``` ### Lossy compression Powered by [pngquant](https://github.com/ImageOptim/libimagequant), converts RGBA images to palette-based 8-bit indexed images, _including_ alpha component. **PngQuantOptions**: ```ts export interface PngQuantOptions { /** default is 70 */ minQuality?: number | undefined | null /** default is 99 */ maxQuality?: number | undefined | null /** * 1- 10 * Faster speeds generate images of lower quality, but may be useful for real-time generation of images. * default: 5 */ speed?: number | undefined | null /** * Number of least significant bits to ignore. * Useful for generating palettes for VGA, 15-bit textures, or other retro platforms. */ posterization?: number | undefined | null } ``` ```ts export function pngQuantize( input: Buffer, options?: PngQuantOptions | undefined | null, signal?: AbortSignal | undefined | null, ): Promise<Buffer> export function pngQuantizeSync(input: Buffer, options?: PngQuantOptions | undefined | null): Buffer ``` ## Optimize JPEG Lossy and Lossless JPEG compression powered by [mozjpeg](https://github.com/mozilla/mozjpeg). **JpegCompressOptions**: ```ts export interface JpegCompressOptions { /** Output quality, default is 100 (lossless) */ quality?: number | undefined | null /** * If true, it will use MozJPEG’s scan optimization. Makes progressive image files smaller. * Default is `true` */ optimizeScans?: boolean | undefined | null } ``` ```ts export function compressJpeg( input: Buffer, options?: JpegCompressOptions | undefined | null, signal?: AbortSignal | undefined | null, ): Promise<Buffer> export function compressJpegSync(input: Buffer, options?: JpegCompressOptions | undefined | null): Buffer ``` ## Credits See [Credits](./credits.md)