pixel-data-js 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.dev.cjs +1405 -70
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +1355 -68
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +1405 -70
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +581 -64
  8. package/dist/index.prod.js +1355 -68
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +14 -3
  11. package/src/Algorithm/floodFillSelection.ts +229 -0
  12. package/src/Canvas/PixelCanvas.ts +31 -0
  13. package/src/Canvas/ReusableCanvas.ts +44 -0
  14. package/src/Canvas/_constants.ts +2 -0
  15. package/src/Clipboard/getImageDataFromClipboard.ts +42 -0
  16. package/src/Clipboard/writeImageDataToClipboard.ts +25 -0
  17. package/src/Clipboard/writeImgBlobToClipboard.ts +13 -0
  18. package/src/ImageData/{extractImageData.ts → extractImageDataPixels.ts} +21 -3
  19. package/src/ImageData/imageDataToAlphaMask.ts +35 -0
  20. package/src/ImageData/imageDataToDataUrl.ts +27 -0
  21. package/src/ImageData/imageDataToImgBlob.ts +31 -0
  22. package/src/ImageData/imgBlobToImageData.ts +52 -0
  23. package/src/ImageData/invertImageData.ts +10 -0
  24. package/src/ImageData/resizeImageData.ts +75 -0
  25. package/src/ImageData/{writeImageData.ts → writeImageDataPixels.ts} +22 -3
  26. package/src/Input/fileInputChangeToImageData.ts +37 -0
  27. package/src/Input/fileToImageData.ts +75 -0
  28. package/src/Input/getSupportedRasterFormats.ts +74 -0
  29. package/src/Mask/extractMask.ts +86 -0
  30. package/src/Mask/mergeMasks.ts +1 -6
  31. package/src/PixelData/blendColorPixelData.ts +9 -9
  32. package/src/PixelData/fillPixelData.ts +51 -12
  33. package/src/PixelData/invertPixelData.ts +16 -0
  34. package/src/PixelData/pixelDataToAlphaMask.ts +28 -0
  35. package/src/Rect/trimRectBounds.ts +118 -0
  36. package/src/_types.ts +37 -20
  37. package/src/blend-modes.ts +506 -66
  38. package/src/color.ts +6 -6
  39. package/src/globals.d.ts +2 -0
  40. package/src/index.ts +37 -1
@@ -1,11 +1,30 @@
1
1
  import type { Rect } from '../_types'
2
2
 
3
- export function writeImageData(
3
+ /**
4
+ * Copies a pixel buffer into a specific region of an {@link ImageData} object.
5
+ *
6
+ * This function performs a direct memory copy from a {@link Uint8ClampedArray}
7
+ * into the target {@link ImageData} buffer. It supports both {@link Rect}
8
+ * objects and discrete coordinates.
9
+ *
10
+ * @param imageData - The target {@link ImageData} to write into. Must match the rect width/height.
11
+ * @param data - The source pixel data (RGBA).
12
+ * @param rect - A {@link Rect} object defining the destination region.
13
+ */
14
+ export function writeImageDataPixels(
4
15
  imageData: ImageData,
5
16
  data: Uint8ClampedArray,
6
17
  rect: Rect,
7
18
  ): void
8
- export function writeImageData(
19
+ /**
20
+ * @param imageData - The target {@link ImageData} to write into.
21
+ * @param data - The source pixel data (RGBA). Must match the width/height.
22
+ * @param x - The starting horizontal coordinate in the target.
23
+ * @param y - The starting vertical coordinate in the target.
24
+ * @param w - The width of the region to write.
25
+ * @param h - The height of the region to write.
26
+ */
27
+ export function writeImageDataPixels(
9
28
  imageData: ImageData,
10
29
  data: Uint8ClampedArray,
11
30
  x: number,
@@ -13,7 +32,7 @@ export function writeImageData(
13
32
  w: number,
14
33
  h: number,
15
34
  ): void
16
- export function writeImageData(
35
+ export function writeImageDataPixels(
17
36
  imageData: ImageData,
18
37
  data: Uint8ClampedArray,
19
38
  _x: Rect | number,
@@ -0,0 +1,37 @@
1
+ import { fileToImageData } from '../../src'
2
+
3
+ /**
4
+ * A convenience wrapper that extracts the first {@link File} from an
5
+ * {@link HTMLInputElement} change event and converts it into {@link ImageData}.
6
+ *
7
+ * This function handles the boilerplate of accessing the file list and checking
8
+ * for existence. It is ideal for use directly in an `onchange` event listener.
9
+ *
10
+ * @param event - The change {@link Event} from an `<input type="file">` element.
11
+ *
12
+ * @returns A promise that resolves to {@link ImageData} if a file was successfully
13
+ * processed, or `null` if no file was selected or the input was cleared.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const input = document.querySelector('input[type="file"]');
18
+ *
19
+ * input.addEventListener('change', async (event) => {
20
+ * const imageData = await fileInputChangeToImageData(event);
21
+ *
22
+ * if (imageData) {
23
+ * console.log('Image loaded:', imageData.width, imageData.height);
24
+ * }
25
+ * });
26
+ * ```
27
+ */
28
+ export async function fileInputChangeToImageData(
29
+ event: Event,
30
+ ): Promise<ImageData | null> {
31
+ const target = event.target as HTMLInputElement
32
+
33
+ const file = target.files?.[0]
34
+ if (!file) return null
35
+
36
+ return await fileToImageData(file)
37
+ }
@@ -0,0 +1,75 @@
1
+ import { OFFSCREEN_CANVAS_CTX_FAILED } from '../Canvas/_constants'
2
+
3
+ /**
4
+ * Thrown when the user provides a file that isn't an image.
5
+ */
6
+ export class UnsupportedFormatError extends Error {
7
+ constructor(mimeType: string) {
8
+ super(`File type ${mimeType} is not a supported image format.`)
9
+ this.name = 'UnsupportedFormatError'
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Converts a browser {@link File} object into {@link ImageData}.
15
+ * This utility handles the full pipeline of image decoding using hardware-accelerated
16
+ * APIs {@link createImageBitmap} and {@link OffscreenCanvas}. It ensures that underlying
17
+ * resources like `ImageBitmap` are properly closed even if the conversion fails.
18
+ *
19
+ * @param file - The image file to convert. Can be null or undefined.
20
+ * @returns A `Promise` resolving to the pixel data as {@link ImageData},
21
+ * or `null` if no file was provided.
22
+ * @throws {@link UnsupportedFormatError}
23
+ * Thrown if the provided file's MIME type does not start with `image/`.
24
+ * @example
25
+ * ```typescript
26
+ * try {
27
+ * const imageData = await fileToImageData(file);
28
+ * if (imageData) {
29
+ * console.log('Pixels:', imageData.data);
30
+ * }
31
+ * } catch (err) {
32
+ * if (err instanceof UnsupportedFormatError) {
33
+ * // Handle bad file type
34
+ * }
35
+ * }
36
+ * ```
37
+ */
38
+ export async function fileToImageData(
39
+ file: File | null | undefined,
40
+ ): Promise<ImageData | null> {
41
+ if (!file) return null
42
+
43
+ if (!file.type.startsWith('image/')) {
44
+ throw new UnsupportedFormatError(file.type)
45
+ }
46
+
47
+ let bitmap: ImageBitmap | null = null
48
+
49
+ try {
50
+ bitmap = await createImageBitmap(file)
51
+
52
+ const canvas = new OffscreenCanvas(
53
+ bitmap.width,
54
+ bitmap.height,
55
+ )
56
+
57
+ const ctx = canvas.getContext('2d')
58
+ if (!ctx) throw new Error(OFFSCREEN_CANVAS_CTX_FAILED)
59
+
60
+ ctx.drawImage(
61
+ bitmap,
62
+ 0,
63
+ 0,
64
+ )
65
+
66
+ return ctx.getImageData(
67
+ 0,
68
+ 0,
69
+ bitmap.width,
70
+ bitmap.height,
71
+ )
72
+ } finally {
73
+ bitmap?.close()
74
+ }
75
+ }
@@ -0,0 +1,74 @@
1
+ // Cache the Promise to prevent race conditions during initialization
2
+ let formatsPromise: Promise<string[]> | null = null
3
+
4
+ const defaultRasterMimes = [
5
+ 'image/png',
6
+ 'image/jpeg',
7
+ 'image/webp',
8
+ 'image/avif',
9
+ 'image/gif',
10
+ 'image/bmp',
11
+ ]
12
+
13
+ /**
14
+ * Probes the browser environment to determine which image MIME types are
15
+ * supported for pixel-level operations.
16
+ * This function performs a one-time check by attempting to convert a
17
+ * {@link OffscreenCanvas} to MIME types. The result is
18
+ * cached to prevent redundant hardware-accelerated operations on
19
+ * subsequent calls.
20
+ * @param rasterMimes List of MIME types to check
21
+ * @default ['image/png',
22
+ * 'image/jpeg',
23
+ * 'image/webp',
24
+ * 'image/avif',
25
+ * 'image/gif',
26
+ * 'image/bmp']
27
+ * @returns A `Promise` resolving to an array of supported MIME
28
+ * types from the `rasterMimes` list.
29
+ * @throws {Error} If the {@link OffscreenCanvas} context cannot be initialized.
30
+ * @example
31
+ * ```typescript
32
+ * const supported = await getSupportedPixelFormats();
33
+ * if (supported.includes('image/avif')) {
34
+ * console.log('High-efficiency formats available');
35
+ * }
36
+ * ```
37
+ */
38
+ export async function getSupportedPixelFormats(rasterMimes = defaultRasterMimes): Promise<string[]> {
39
+ if (formatsPromise) {
40
+ return formatsPromise
41
+ }
42
+
43
+ const probeCanvas = async () => {
44
+ const canvas = new OffscreenCanvas(1, 1)
45
+
46
+ const results = await Promise.all(
47
+ rasterMimes.map(async (mime) => {
48
+ try {
49
+ const blob = await canvas.convertToBlob({
50
+ type: mime,
51
+ })
52
+
53
+ return blob.type === mime ? mime : null
54
+ } catch {
55
+ return null
56
+ }
57
+ }),
58
+ )
59
+
60
+ return results.filter((type): type is string => {
61
+ return type !== null
62
+ })
63
+ }
64
+
65
+ // By chaining .catch here, the microtask guarantees formatsPromise
66
+ // is assigned the promise BEFORE the catch block runs to reset it.
67
+ formatsPromise = probeCanvas().catch((error) => {
68
+ formatsPromise = null
69
+
70
+ throw error
71
+ })
72
+
73
+ return formatsPromise
74
+ }
@@ -0,0 +1,86 @@
1
+ import type { Rect } from '../_types'
2
+
3
+ /**
4
+ * Extracts a rectangular region from a 1D {@link Uint8Array} mask.
5
+ * This utility calculates the necessary offsets based on the `maskWidth` to
6
+ * slice out a specific area.
7
+ *
8
+ * @param mask - The source 1D array representing the full 2D mask.
9
+ * @param maskWidth - The width of the original source mask (stride).
10
+ * @param rect - A {@link Rect} object defining the region to extract.
11
+ * @returns A new {@link Uint8Array} containing the extracted region.
12
+ */
13
+ export function extractMask(
14
+ mask: Uint8Array,
15
+ maskWidth: number,
16
+ rect: Rect,
17
+ ): Uint8Array
18
+ /**
19
+ * @param mask - The source 1D array representing the full 2D mask.
20
+ * @param maskWidth - The width of the original source mask (stride).
21
+ * @param x - The starting horizontal coordinate.
22
+ * @param y - The starting vertical coordinate.
23
+ * @param w - The width of the region to extract.
24
+ * @param h - The height of the region to extract.
25
+ * @returns A new {@link Uint8Array} containing the extracted region.
26
+ */
27
+ export function extractMask(
28
+ mask: Uint8Array,
29
+ maskWidth: number,
30
+ x: number,
31
+ y: number,
32
+ w: number,
33
+ h: number,
34
+ ): Uint8Array
35
+ export function extractMask(
36
+ mask: Uint8Array,
37
+ maskWidth: number,
38
+ xOrRect: number | Rect,
39
+ y?: number,
40
+ w?: number,
41
+ h?: number,
42
+ ): Uint8Array {
43
+ let finalX: number
44
+ let finalY: number
45
+ let finalW: number
46
+ let finalH: number
47
+
48
+ if (typeof xOrRect === 'object') {
49
+ finalX = xOrRect.x
50
+ finalY = xOrRect.y
51
+ finalW = xOrRect.w
52
+ finalH = xOrRect.h
53
+ } else {
54
+ finalX = xOrRect
55
+ finalY = y!
56
+ finalW = w!
57
+ finalH = h!
58
+ }
59
+
60
+ const out = new Uint8Array(finalW * finalH)
61
+ const srcH = mask.length / maskWidth
62
+
63
+ for (let row = 0; row < finalH; row++) {
64
+ const currentSrcY = finalY + row
65
+
66
+ if (currentSrcY < 0 || currentSrcY >= srcH) {
67
+ continue
68
+ }
69
+
70
+ const start = Math.max(0, finalX)
71
+ const end = Math.min(maskWidth, finalX + finalW)
72
+
73
+ if (start < end) {
74
+ const srcOffset = currentSrcY * maskWidth + start
75
+ const dstOffset = (row * finalW) + (start - finalX)
76
+ const count = end - start
77
+
78
+ out.set(
79
+ mask.subarray(srcOffset, srcOffset + count),
80
+ dstOffset,
81
+ )
82
+ }
83
+ }
84
+
85
+ return out
86
+ }
@@ -1,9 +1,4 @@
1
- import {
2
- type AnyMask,
3
- type AlphaMask,
4
- type ApplyMaskOptions,
5
- MaskType,
6
- } from '../_types'
1
+ import { type AlphaMask, type AnyMask, type ApplyMaskOptions, MaskType } from '../_types'
7
2
 
8
3
  /**
9
4
  * Merges a source mask into a destination AlphaMask.
@@ -82,16 +82,16 @@ export function blendColorPixelData(
82
82
  const mVal = mask[mIdx]
83
83
 
84
84
  if (isAlphaMask) {
85
- const effectiveM = invertMask
86
- ? 255 - mVal
87
- : mVal
85
+ const effectiveM = invertMask
86
+ ? 255 - mVal
87
+ : mVal
88
88
 
89
- // If mask is transparent, skip
90
- if (effectiveM === 0) {
91
- dIdx++
92
- mIdx++
93
- continue
94
- }
89
+ // If mask is transparent, skip
90
+ if (effectiveM === 0) {
91
+ dIdx++
92
+ mIdx++
93
+ continue
94
+ }
95
95
 
96
96
  // globalAlpha is not a factor
97
97
  if (globalAlpha === 255) {
@@ -2,24 +2,63 @@ import type { Color32, Rect } from '../_types'
2
2
  import type { PixelData } from '../PixelData'
3
3
 
4
4
  /**
5
- * A high-performance solid fill for PixelData.
5
+ * Fills a region or the {@link PixelData} buffer with a solid color.
6
+ *
7
+ * @param dst - The target {@link PixelData} to modify.
8
+ * @param color - The {@link Color32} value to apply.
9
+ * @param rect - A {@link Rect} defining the area to fill. If omitted, the entire
10
+ * buffer is filled.
6
11
  */
7
12
  export function fillPixelData(
8
13
  dst: PixelData,
9
14
  color: Color32,
10
15
  rect?: Partial<Rect>,
16
+ ): void
17
+ /**
18
+ * @param dst - The target {@link PixelData} to modify.
19
+ * @param color - The {@link Color32} value to apply.
20
+ * @param x - Starting horizontal coordinate.
21
+ * @param y - Starting vertical coordinate.
22
+ * @param w - Width of the fill area.
23
+ * @param h - Height of the fill area.
24
+ */
25
+ export function fillPixelData(
26
+ dst: PixelData,
27
+ color: Color32,
28
+ x: number,
29
+ y: number,
30
+ w: number,
31
+ h: number,
32
+ ): void
33
+ export function fillPixelData(
34
+ dst: PixelData,
35
+ color: Color32,
36
+ _x?: Partial<Rect> | number,
37
+ _y?: number,
38
+ _w?: number,
39
+ _h?: number,
11
40
  ): void {
12
- const {
13
- x: targetX = 0,
14
- y: targetY = 0,
15
- w: width = dst.width,
16
- h: height = dst.height,
17
- } = rect || {}
18
-
19
- let x = targetX
20
- let y = targetY
21
- let w = width
22
- let h = height
41
+ let x: number
42
+ let y: number
43
+ let w: number
44
+ let h: number
45
+
46
+ if (typeof _x === 'object') {
47
+ x = _x.x ?? 0
48
+ y = _x.y ?? 0
49
+ w = _x.w ?? dst.width
50
+ h = _x.h ?? dst.height
51
+ } else if (typeof _x === 'number') {
52
+ x = _x
53
+ y = _y!
54
+ w = _w!
55
+ h = _h!
56
+ } else {
57
+ x = 0
58
+ y = 0
59
+ w = dst.width
60
+ h = dst.height
61
+ }
23
62
 
24
63
  // Destination Clipping
25
64
  if (x < 0) {
@@ -0,0 +1,16 @@
1
+ import type { PixelData } from '../PixelData'
2
+
3
+ export function invertPixelData(
4
+ pixelData: PixelData,
5
+ ): PixelData {
6
+
7
+ const data32 = pixelData.data32
8
+ const len = data32.length
9
+
10
+ for (let i = 0; i < len; i++) {
11
+ // XOR with 0x00FFFFFF flips RGB bits and ignores Alpha (the top 8 bits)
12
+ data32[i] = data32[i] ^ 0x00FFFFFF
13
+ }
14
+
15
+ return pixelData
16
+ }
@@ -0,0 +1,28 @@
1
+ import type { AlphaMask } from '../_types'
2
+ import type { PixelData } from '../PixelData'
3
+
4
+ /**
5
+ * Extracts the alpha channel from PixelData into a single-channel mask.
6
+ * Returns a Uint8Array branded as AlphaMask.
7
+ */
8
+ export function pixelDataToAlphaMask(
9
+ pixelData: PixelData,
10
+ ): AlphaMask {
11
+ const {
12
+ data32,
13
+ width,
14
+ height,
15
+ } = pixelData
16
+ const len = data32.length
17
+ const mask = new Uint8Array(width * height) as AlphaMask
18
+
19
+ for (let i = 0; i < len; i++) {
20
+ const val = data32[i]
21
+
22
+ // Extract the Alpha byte (top 8 bits in ABGR / Little-Endian)
23
+ // Shift right by 24 moves the 4th byte to the 1st position
24
+ mask[i] = (val >>> 24) & 0xff
25
+ }
26
+
27
+ return mask
28
+ }
@@ -0,0 +1,118 @@
1
+ import type { Rect, SelectionRect } from '../_types'
2
+ import { extractMask } from '../Mask/extractMask'
3
+
4
+ /**
5
+ * Intersects a target rectangle with a boundary, trimming dimensions and masks in-place.
6
+ * This utility calculates the axis-aligned intersection between the `target` and `bounds`.
7
+ * If the `target` includes a `mask` (as in a {@link SelectionRect}), the mask is physically
8
+ * cropped and re-aligned using `extractMask` to match the new dimensions.
9
+ * @param target - The rectangle or selection object to be trimmed. **Note:** This object is mutated in-place.
10
+ * @param bounds - The boundary rectangle defining the maximum allowable area (e.g., canvas dimensions).
11
+ * @example
12
+ * const selection = { x: -10, y: -10, w: 50, h: 50, mask: new Uint8Array(2500) };
13
+ * const canvas = { x: 0, y: 0, w: 100, h: 100 };
14
+ * // Selection will be moved to (0,0) and resized to 40x40.
15
+ * // The mask is cropped by 10 px on the top and left.
16
+ * trimRectBounds(selection, canvas);
17
+ */
18
+ export function trimRectBounds<T extends Rect | SelectionRect>(
19
+ target: T,
20
+ bounds: Rect,
21
+ ): void {
22
+ const originalX = target.x
23
+ const originalY = target.y
24
+ const originalW = target.w
25
+
26
+ const intersectedX = Math.max(target.x, bounds.x)
27
+ const intersectedY = Math.max(target.y, bounds.y)
28
+
29
+ const intersectedMaxX = Math.min(
30
+ target.x + target.w,
31
+ bounds.x + bounds.w,
32
+ )
33
+ const intersectedMaxY = Math.min(
34
+ target.y + target.h,
35
+ bounds.y + bounds.h,
36
+ )
37
+
38
+ // Intersection check
39
+ if (intersectedMaxX <= intersectedX || intersectedMaxY <= intersectedY) {
40
+ target.w = 0
41
+ target.h = 0
42
+
43
+ if ('mask' in target && target.mask) {
44
+ // This line is now hit by the 'empty intersection' test below
45
+ target.mask = new Uint8Array(0)
46
+ }
47
+
48
+ return
49
+ }
50
+
51
+ const intersectedW = intersectedMaxX - intersectedX
52
+ const intersectedH = intersectedMaxY - intersectedY
53
+ const offsetX = intersectedX - originalX
54
+ const offsetY = intersectedY - originalY
55
+
56
+ target.x = intersectedX
57
+ target.y = intersectedY
58
+ target.w = intersectedW
59
+ target.h = intersectedH
60
+
61
+ if ('mask' in target && target.mask) {
62
+ const currentMask = extractMask(
63
+ target.mask,
64
+ originalW,
65
+ offsetX,
66
+ offsetY,
67
+ intersectedW,
68
+ intersectedH,
69
+ )
70
+
71
+ let minX = intersectedW
72
+ let maxX = -1
73
+ let minY = intersectedH
74
+ let maxY = -1
75
+
76
+ // Scan for content
77
+ for (let y = 0; y < intersectedH; y++) {
78
+ for (let x = 0; x < intersectedW; x++) {
79
+ if (currentMask[y * intersectedW + x] !== 0) {
80
+ if (x < minX) minX = x
81
+ if (x > maxX) maxX = x
82
+ if (y < minY) minY = y
83
+ if (y > maxY) maxY = y
84
+ }
85
+ }
86
+ }
87
+
88
+ // If no content is found (all zeros)
89
+ if (maxX === -1) {
90
+ target.w = 0
91
+ target.h = 0
92
+ // This covers the specific line you mentioned
93
+ target.mask = new Uint8Array(0)
94
+ return
95
+ }
96
+
97
+ const finalW = maxX - minX + 1
98
+ const finalH = maxY - minY + 1
99
+
100
+ // Only shift and crop if the content is smaller than the intersection
101
+ if (finalW !== intersectedW || finalH !== intersectedH) {
102
+ target.mask = extractMask(
103
+ currentMask,
104
+ intersectedW,
105
+ minX,
106
+ minY,
107
+ finalW,
108
+ finalH,
109
+ )
110
+ target.x += minX
111
+ target.y += minY
112
+ target.w = finalW
113
+ target.h = finalH
114
+ } else {
115
+ target.mask = currentMask
116
+ }
117
+ }
118
+ }