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
@@ -0,0 +1,229 @@
1
+ import { type Color32, type ImageDataLike, MaskType, type Rect, type SelectionRect } from '../_types'
2
+ import { colorDistance } from '../color'
3
+ import { extractImageDataPixels } from '../ImageData/extractImageDataPixels'
4
+ import type { PixelData } from '../PixelData'
5
+ import { trimRectBounds } from '../Rect/trimRectBounds'
6
+
7
+ export type FloodFillImageDataOptions = {
8
+ contiguous?: boolean
9
+ tolerance?: number
10
+ bounds?: Rect
11
+ }
12
+
13
+ export type FloodFillResult = {
14
+ startX: number
15
+ startY: number
16
+ selectionRect: SelectionRect
17
+ pixels: Uint8ClampedArray
18
+ }
19
+
20
+ /**
21
+ * Performs a color-based flood fill selection on {@link ImageData} or {@link PixelData}.
22
+ * This utility identifies pixels starting from a specific coordinate that fall within a
23
+ * color tolerance. It can operate in "contiguous" mode (classic bucket fill) or
24
+ * "non-contiguous" mode (selects all matching pixels in the buffer).
25
+ *
26
+ * @param img - The source image data to process.
27
+ * @param startX - The starting horizontal coordinate.
28
+ * @param startY - The starting vertical coordinate.
29
+ * @param options - Configuration for the fill operation.
30
+ * @param options.contiguous - @default true. If true, only connected pixels are
31
+ * selected. If false, all pixels within tolerance are selected regardless of position.
32
+ * @param options.tolerance - @default 0. The maximum allowed difference in color
33
+ * distance (0-255) for a pixel to be included.
34
+ * @param options.bounds - Optional bounding box to restrict the search area.
35
+ *
36
+ * @returns A {@link FloodFillResult} containing the mask and bounds of the selection,
37
+ * or `null` if the starting coordinates are out of bounds.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const result = floodFillImageDataSelection(
42
+ * ctx.getImageData(0, 0, 100, 100),
43
+ * 50,
44
+ * 50,
45
+ * {
46
+ * tolerance: 20,
47
+ * contiguous: true
48
+ * }
49
+ * );
50
+ * ```
51
+ */
52
+ export function floodFillSelection(
53
+ img: ImageDataLike | PixelData,
54
+ startX: number,
55
+ startY: number,
56
+ {
57
+ contiguous = true,
58
+ tolerance = 0,
59
+ bounds,
60
+ }: FloodFillImageDataOptions = {},
61
+ ): FloodFillResult | null {
62
+
63
+ let imageData: ImageDataLike
64
+ let data32: Uint32Array
65
+ if ('data32' in img) {
66
+ data32 = img.data32
67
+ imageData = img.imageData
68
+ } else {
69
+ data32 = new Uint32Array(
70
+ img.data.buffer,
71
+ img.data.byteOffset,
72
+ img.data.byteLength >> 2,
73
+ )
74
+ imageData = img
75
+ }
76
+ const {
77
+ width,
78
+ height,
79
+ } = img
80
+
81
+ const limit = bounds || {
82
+ x: 0,
83
+ y: 0,
84
+ w: width,
85
+ h: height,
86
+ }
87
+
88
+ const xMin = Math.max(0, limit.x)
89
+ const xMax = Math.min(width - 1, limit.x + limit.w - 1)
90
+ const yMin = Math.max(0, limit.y)
91
+ const yMax = Math.min(height - 1, limit.y + limit.h - 1)
92
+
93
+ if (startX < xMin || startX > xMax || startY < yMin || startY > yMax) {
94
+ return null
95
+ }
96
+
97
+ const baseColor = data32[startY * width + startX] as Color32
98
+
99
+ let matchCount = 0
100
+ const matchX = new Uint16Array(width * height)
101
+ const matchY = new Uint16Array(width * height)
102
+
103
+ let minX = startX
104
+ let maxX = startX
105
+ let minY = startY
106
+ let maxY = startY
107
+
108
+ if (contiguous) {
109
+ const visited = new Uint8Array(width * height)
110
+ const stack = new Uint32Array(width * height)
111
+ let stackPtr = 0
112
+
113
+ stack[stackPtr++] = (startY << 16) | startX
114
+ visited[startY * width + startX] = 1
115
+
116
+ while (stackPtr > 0) {
117
+ const val = stack[--stackPtr]
118
+ const x = val & 0xFFFF
119
+ const y = val >>> 16
120
+
121
+ matchX[matchCount] = x
122
+ matchY[matchCount] = y
123
+ matchCount++
124
+
125
+ if (x < minX) minX = x
126
+ if (x > maxX) maxX = x
127
+ if (y < minY) minY = y
128
+ if (y > maxY) maxY = y
129
+
130
+ // Right
131
+ if (x + 1 <= xMax) {
132
+ const idx = y * width + (x + 1)
133
+ if (!visited[idx] && colorDistance(data32[idx] as Color32, baseColor) <= tolerance) {
134
+ visited[idx] = 1
135
+ stack[stackPtr++] = (y << 16) | (x + 1)
136
+ }
137
+ }
138
+ // Left
139
+ if (x - 1 >= xMin) {
140
+ const idx = y * width + (x - 1)
141
+ if (!visited[idx] && colorDistance(data32[idx] as Color32, baseColor) <= tolerance) {
142
+ visited[idx] = 1
143
+ stack[stackPtr++] = (y << 16) | (x - 1)
144
+ }
145
+ }
146
+ // Down
147
+ if (y + 1 <= yMax) {
148
+ const idx = (y + 1) * width + x
149
+ if (!visited[idx] && colorDistance(data32[idx] as Color32, baseColor) <= tolerance) {
150
+ visited[idx] = 1
151
+ stack[stackPtr++] = ((y + 1) << 16) | x
152
+ }
153
+ }
154
+ // Up
155
+ if (y - 1 >= yMin) {
156
+ const idx = (y - 1) * width + x
157
+ if (!visited[idx] && colorDistance(data32[idx] as Color32, baseColor) <= tolerance) {
158
+ visited[idx] = 1
159
+ stack[stackPtr++] = ((y - 1) << 16) | x
160
+ }
161
+ }
162
+ }
163
+ } else {
164
+ for (let y = yMin; y <= yMax; y++) {
165
+ for (let x = xMin; x <= xMax; x++) {
166
+ const color = data32[y * width + x] as Color32
167
+ if (colorDistance(color, baseColor) <= tolerance) {
168
+ matchX[matchCount] = x
169
+ matchY[matchCount] = y
170
+ matchCount++
171
+
172
+ if (x < minX) minX = x
173
+ if (x > maxX) maxX = x
174
+ if (y < minY) minY = y
175
+ if (y > maxY) maxY = y
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ if (matchCount === 0) {
182
+ return null
183
+ }
184
+ const selectionRect: SelectionRect = {
185
+ x: minX,
186
+ y: minY,
187
+ w: maxX - minX + 1,
188
+ h: maxY - minY + 1,
189
+ mask: new Uint8Array((maxX - minX + 1) * (maxY - minY + 1)),
190
+ maskType: MaskType.BINARY,
191
+ }
192
+
193
+ // REMOVED trimRectBounds from here
194
+
195
+ const sw = selectionRect.w
196
+ const sh = selectionRect.h
197
+ const finalMask = selectionRect.mask!
198
+
199
+ for (let i = 0; i < matchCount; i++) {
200
+ const mx = matchX[i] - selectionRect.x
201
+ const my = matchY[i] - selectionRect.y
202
+
203
+ if (mx >= 0 && mx < sw && my >= 0 && my < sh) {
204
+ finalMask[my * sw + mx] = 1
205
+ }
206
+ }
207
+
208
+ // trimRectBounds can see them and work correctly.
209
+ trimRectBounds(
210
+ selectionRect,
211
+ { x: 0, y: 0, w: width, h: height },
212
+ )
213
+
214
+ // Use the UPDATED values from the selectionRect after trimming
215
+ const extracted = extractImageDataPixels(
216
+ imageData,
217
+ selectionRect.x,
218
+ selectionRect.y,
219
+ selectionRect.w,
220
+ selectionRect.h,
221
+ )
222
+
223
+ return {
224
+ startX,
225
+ startY,
226
+ selectionRect,
227
+ pixels: extracted,
228
+ }
229
+ }
@@ -0,0 +1,31 @@
1
+ import { CANVAS_CTX_FAILED } from './_constants'
2
+
3
+ export type PixelCanvas = {
4
+ readonly canvas: HTMLCanvasElement,
5
+ readonly ctx: CanvasRenderingContext2D,
6
+ readonly resize: (w: number, h: number) => void
7
+ }
8
+
9
+ /**
10
+ * Ensures the canvas ctx is always set to imageSmoothingEnabled = false.
11
+ * Intended for canvas elements that are already part of the DOM.
12
+ * @see makeReusableCanvas
13
+ * @throws {Error} If the {@link HTMLCanvasElement} context cannot be initialized.
14
+ */
15
+ export function makePixelCanvas(
16
+ canvas: HTMLCanvasElement,
17
+ ): PixelCanvas {
18
+ const ctx = canvas.getContext('2d')
19
+ if (!ctx) throw new Error(CANVAS_CTX_FAILED)
20
+ ctx.imageSmoothingEnabled = false
21
+
22
+ return {
23
+ canvas,
24
+ ctx,
25
+ resize(w: number, h: number) {
26
+ canvas.width = w
27
+ canvas.height = h
28
+ ctx.imageSmoothingEnabled = false
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,44 @@
1
+ import { CANVAS_CTX_FAILED } from './_constants'
2
+
3
+ export type ReusableCanvas = {
4
+ readonly canvas: HTMLCanvasElement
5
+ readonly ctx: CanvasRenderingContext2D
6
+ }
7
+
8
+ /**
9
+ * Creates a reusable canvas and context that are not part of the DOM.
10
+ * Ensures it is always set to `context.imageSmoothingEnabled = false`
11
+ * @see makePixelCanvas
12
+ * @throws {Error} If the {@link HTMLCanvasElement} context cannot be initialized.
13
+ */
14
+ export function makeReusableCanvas() {
15
+ let canvas: HTMLCanvasElement | null = null
16
+ let ctx: CanvasRenderingContext2D | null = null
17
+
18
+ function get(width: number, height: number): ReusableCanvas {
19
+ if (canvas === null) {
20
+ canvas = document.createElement('canvas')!
21
+ ctx = canvas.getContext('2d')!
22
+ if (!ctx) throw new Error(CANVAS_CTX_FAILED)
23
+ }
24
+
25
+ // Resize if needed (resizing auto-clears)
26
+ if (canvas.width !== width || canvas.height !== height) {
27
+ canvas.width = width
28
+ canvas.height = height
29
+ ctx!.imageSmoothingEnabled = false
30
+ } else {
31
+ // Same size → manually clear
32
+ ctx!.clearRect(0, 0, width, height)
33
+ }
34
+
35
+ return { canvas, ctx: ctx! }
36
+ }
37
+
38
+ get.reset = () => {
39
+ canvas = null
40
+ ctx = null
41
+ }
42
+
43
+ return get
44
+ }
@@ -0,0 +1,2 @@
1
+ export const OFFSCREEN_CANVAS_CTX_FAILED = 'Failed to create OffscreenCanvas context'
2
+ export const CANVAS_CTX_FAILED = 'Failed to create Canvas context'
@@ -0,0 +1,42 @@
1
+ import { imgBlobToImageData } from '../ImageData/imgBlobToImageData'
2
+
3
+ /**
4
+ * Extracts {@link ImageData} from a clipboard event if an image is present.
5
+ *
6
+ * This function iterates through the {@link DataTransferItemList} to find
7
+ * the first item with an image MIME type and decodes it.
8
+ *
9
+ * @param clipboardEvent - The event object from a `paste` listener.
10
+ *
11
+ * @returns A promise resolving to {@link ImageData}, or `null` if no
12
+ * image was found in the clipboard.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * window.addEventListener('paste', async (event) => {
17
+ * const data = await getImageDataFromClipboard(event)
18
+ * if (data) {
19
+ * console.log('Pasted image dimensions:', data.width, data.height)
20
+ * }
21
+ * });
22
+ * ```
23
+ */
24
+ export async function getImageDataFromClipboard(clipboardEvent: ClipboardEvent) {
25
+ const items = clipboardEvent?.clipboardData?.items
26
+ if (!items?.length) return null
27
+
28
+ for (let i = 0; i < items.length; i++) {
29
+ const item = items[i]
30
+
31
+ if (item.type.startsWith('image/')) {
32
+ const blob = item.getAsFile()
33
+
34
+ if (!blob) {
35
+ continue
36
+ }
37
+
38
+ return imgBlobToImageData(blob)
39
+ }
40
+ }
41
+ return null
42
+ }
@@ -0,0 +1,25 @@
1
+ import { imageDataToImgBlob } from '../ImageData/imageDataToImgBlob'
2
+ import { writeImgBlobToClipboard } from './writeImgBlobToClipboard'
3
+
4
+ /**
5
+ * Converts {@link ImageData} to a PNG {@link Blob} and writes it to the system clipboard.
6
+ * This is a high-level utility that combines {@link imageDataToImgBlob} and
7
+ * {@link writeImgBlobToClipboard}.
8
+ * @param imageData - The image data to copy to the clipboard.
9
+ * @returns A promise that resolves when the image has been successfully copied.
10
+ * @throws {Error}
11
+ * If the conversion to blob fails or clipboard permissions are denied.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const canvas = document.querySelector('canvas')
16
+ * const ctx = canvas.getContext('2d')
17
+ * const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
18
+ * await writeImageDataToClipboard(imageData)
19
+ * ```
20
+ */
21
+ export async function writeImageDataToClipboard(imageData: ImageData): Promise<void> {
22
+ const blob = await imageDataToImgBlob(imageData)
23
+
24
+ return writeImgBlobToClipboard(blob)
25
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Writes a {@link Blob} image to the system clipboard.
3
+ *
4
+ * @param blob - The image {@link Blob} (typically `image/png`) to copy.
5
+ * @returns A promise that resolves when the clipboard has been updated.
6
+ */
7
+ export async function writeImgBlobToClipboard(blob: Blob): Promise<void> {
8
+ const item = new ClipboardItem({
9
+ 'image/png': blob,
10
+ })
11
+
12
+ await navigator.clipboard.write([item])
13
+ }
@@ -1,17 +1,35 @@
1
1
  import type { ImageDataLike, Rect } from '../_types'
2
2
 
3
- export function extractImageData(
3
+ /**
4
+ * Extracts a specific rectangular region of pixels from a larger {@link ImageDataLike}
5
+ * source into a new {@link Uint8ClampedArray}.
6
+ *
7
+ * This is a "read-only" operation that returns a copy of the pixel data.
8
+ *
9
+ * @param imageData - The source image data to read from.
10
+ * @param rect - A {@link Rect} object defining the region to extract.
11
+ * @returns A {@link Uint8ClampedArray} containing the RGBA pixel data of the region.
12
+ */
13
+ export function extractImageDataPixels(
4
14
  imageData: ImageDataLike,
5
15
  rect: Rect,
6
16
  ): Uint8ClampedArray
7
- export function extractImageData(
17
+ /**
18
+ * @param imageData - The source image data to read from.
19
+ * @param x - The starting horizontal coordinate.
20
+ * @param y - The starting vertical coordinate.
21
+ * @param w - The width of the region to extract.
22
+ * @param h - The height of the region to extract.
23
+ * @returns A {@link Uint8ClampedArray} containing the RGBA pixel data of the region.
24
+ */
25
+ export function extractImageDataPixels(
8
26
  imageData: ImageDataLike,
9
27
  x: number,
10
28
  y: number,
11
29
  w: number,
12
30
  h: number,
13
31
  ): Uint8ClampedArray
14
- export function extractImageData(
32
+ export function extractImageDataPixels(
15
33
  imageData: ImageDataLike,
16
34
  _x: Rect | number,
17
35
  _y?: number,
@@ -0,0 +1,35 @@
1
+ import type { AlphaMask } from '../_types'
2
+ import { pixelDataToAlphaMask } from '../PixelData/pixelDataToAlphaMask'
3
+
4
+ /**
5
+ * Extracts the alpha channel from raw ImageData into an AlphaMask.
6
+ * When possible use {@link pixelDataToAlphaMask} instead.
7
+ * Repeat calls to the same data will use less memory.
8
+ */
9
+ export function imageDataToAlphaMask(
10
+ imageData: ImageData,
11
+ ): AlphaMask {
12
+ const {
13
+ width,
14
+ height,
15
+ data,
16
+ } = imageData
17
+
18
+ // Create a 32-bit view of the existing buffer
19
+ const data32 = new Uint32Array(
20
+ data.buffer,
21
+ data.byteOffset,
22
+ data.byteLength >> 2,
23
+ )
24
+ const len = data32.length
25
+ const mask = new Uint8Array(width * height) as AlphaMask
26
+
27
+ for (let i = 0; i < len; i++) {
28
+ const val = data32[i]
29
+
30
+ // Extract Alpha (top 8 bits in Little-Endian/ABGR)
31
+ mask[i] = (val >>> 24) & 0xff
32
+ }
33
+
34
+ return mask
35
+ }
@@ -0,0 +1,27 @@
1
+ import { makeReusableCanvas } from '../Canvas/ReusableCanvas'
2
+
3
+ const get = makeReusableCanvas()
4
+
5
+ /**
6
+ * Converts an {@link ImageData} object into a base64-encoded Data URL string.
7
+ *
8
+ * @param imageData - The pixel data to be converted.
9
+ *
10
+ * @returns A string representing the image in `image/png` format as a
11
+ * [Data URL](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data).
12
+ * @throws {Error} If the {@link HTMLCanvasElement} context cannot be initialized.
13
+ * @example
14
+ * ```typescript
15
+ * const dataUrl = imageDataToDataUrl(imageData);
16
+ * const img = new Image();
17
+ * img.src = dataUrl;
18
+ * ```
19
+ */
20
+ export function imageDataToDataUrl(imageData: ImageData): string {
21
+ const { canvas, ctx } = get(imageData.width, imageData.height)
22
+
23
+ ctx.putImageData(imageData, 0, 0)
24
+ return canvas.toDataURL()
25
+ }
26
+
27
+ imageDataToDataUrl.reset = get.reset
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Converts an {@link ImageData} object into a {@link Blob} in PNG format.
3
+ *
4
+ * This operation is asynchronous and uses {@link OffscreenCanvas}
5
+ * to perform the encoding, making it suitable for usage in both the main
6
+ * thread and Web Workers.
7
+ *
8
+ * @param imageData - The pixel data to be encoded.
9
+ *
10
+ * @returns A promise that resolves to a {@link Blob} with the MIME type `image/png`.
11
+ *
12
+ * @throws {Error}
13
+ * Thrown if the {@link OffscreenCanvas} context cannot be initialized or the blob
14
+ * encoding fails.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const blob = await imageDataToImgBlob(imageData);
19
+ * const url = URL.createObjectURL(blob);
20
+ * ```
21
+ */
22
+ export async function imageDataToImgBlob(imageData: ImageData): Promise<Blob> {
23
+ const canvas = new OffscreenCanvas(imageData.width, imageData.height)
24
+ const ctx = canvas.getContext('2d')
25
+ if (!ctx) throw new Error('could not create 2d context')
26
+
27
+ ctx.putImageData(imageData, 0, 0)
28
+ return canvas!.convertToBlob({
29
+ type: 'image/png',
30
+ })
31
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Decodes a {@link Blob} (typically PNG) back into an {@link ImageData} object.
3
+ *
4
+ * This function uses hardware-accelerated decoding via {@link createImageBitmap}
5
+ * and processes the data using an {@link OffscreenCanvas} to ensure
6
+ * compatibility with Web Workers.
7
+ *
8
+ * @param blob - The binary image data to decode.
9
+ *
10
+ * @returns A promise resolving to the decoded {@link ImageData}.
11
+ *
12
+ * @throws {Error}
13
+ * Thrown if the blob is corrupted or the browser cannot decode the format.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const blob = await getBlobFromStorage();
18
+ *
19
+ * const imageData = await pngBlobToImageData(blob);
20
+ * ```
21
+ */
22
+ export async function imgBlobToImageData(
23
+ blob: Blob,
24
+ ): Promise<ImageData> {
25
+ let bitmap: ImageBitmap | null = null
26
+
27
+ try {
28
+ bitmap = await createImageBitmap(blob)
29
+
30
+ const canvas = new OffscreenCanvas(
31
+ bitmap.width,
32
+ bitmap.height,
33
+ )
34
+
35
+ const ctx = canvas.getContext('2d')
36
+
37
+ if (!ctx) {
38
+ throw new Error('Failed to get 2D context')
39
+ }
40
+
41
+ ctx.drawImage(bitmap, 0, 0)
42
+
43
+ return ctx.getImageData(
44
+ 0,
45
+ 0,
46
+ bitmap.width,
47
+ bitmap.height,
48
+ )
49
+ } finally {
50
+ bitmap?.close()
51
+ }
52
+ }
@@ -0,0 +1,10 @@
1
+ export function invertImageData(imageData: ImageData) {
2
+ const data = imageData.data
3
+ let length = data.length
4
+ for (let i = 0; i < length; i += 4) {
5
+ data[i] = 255 - data[i]!
6
+ data[i + 1] = 255 - data[i + 1]!
7
+ data[i + 2] = 255 - data[i + 2]!
8
+ }
9
+ return imageData
10
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Non destructively resizes the {@link ImageData} buffer to new dimensions, optionally
3
+ * offsetting the original content.
4
+ * This operation creates a new buffer. It does not scale or stretch pixels;
5
+ * instead, it crops or pads the image based on the new dimensions and
6
+ * provides an offset for repositioning.
7
+ *
8
+ * @param current The source {@link ImageData} to resize.
9
+ * @param newWidth The target width in pixels.
10
+ * @param newHeight The target height in pixels.
11
+ * @param offsetX The horizontal offset for placing the
12
+ * original image within the new buffer.
13
+ * @default 0
14
+ * @param offsetY The vertical offset for placing the
15
+ * original image within the new buffer.
16
+ * @default 0
17
+ *
18
+ * @returns A new {@link ImageData} instance with the specified dimensions.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * // Centers an 80x80 image in a new 100x100 buffer
23
+ * const resized = resizeImageData(
24
+ * originalData,
25
+ * 100,
26
+ * 100,
27
+ * 10,
28
+ * 10
29
+ * );
30
+ * ```
31
+ */
32
+ export function resizeImageData(
33
+ current: ImageData,
34
+ newWidth: number,
35
+ newHeight: number,
36
+ offsetX = 0,
37
+ offsetY = 0,
38
+ ): ImageData {
39
+ const result = new ImageData(newWidth, newHeight)
40
+ const {
41
+ width: oldW,
42
+ height: oldH,
43
+ data: oldData,
44
+ } = current
45
+ const newData = result.data
46
+
47
+ // Determine intersection of the old image (at offset) and new canvas bounds
48
+ const x0 = Math.max(0, offsetX)
49
+ const y0 = Math.max(0, offsetY)
50
+ const x1 = Math.min(newWidth, offsetX + oldW)
51
+ const y1 = Math.min(newHeight, offsetY + oldH)
52
+
53
+ if (x1 <= x0 || y1 <= y0) {
54
+ return result
55
+ }
56
+
57
+ const rowCount = y1 - y0
58
+ const rowLen = (x1 - x0) * 4
59
+
60
+ for (let row = 0; row < rowCount; row++) {
61
+ const dstY = y0 + row
62
+ const srcY = dstY - offsetY
63
+ const srcX = x0 - offsetX
64
+
65
+ const dstStart = (dstY * newWidth + x0) * 4
66
+ const srcStart = (srcY * oldW + srcX) * 4
67
+
68
+ newData.set(
69
+ oldData.subarray(srcStart, srcStart + rowLen),
70
+ dstStart,
71
+ )
72
+ }
73
+
74
+ return result
75
+ }