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.
- package/dist/index.dev.cjs +1405 -70
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1355 -68
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1405 -70
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +581 -64
- package/dist/index.prod.js +1355 -68
- package/dist/index.prod.js.map +1 -1
- package/package.json +14 -3
- package/src/Algorithm/floodFillSelection.ts +229 -0
- package/src/Canvas/PixelCanvas.ts +31 -0
- package/src/Canvas/ReusableCanvas.ts +44 -0
- package/src/Canvas/_constants.ts +2 -0
- package/src/Clipboard/getImageDataFromClipboard.ts +42 -0
- package/src/Clipboard/writeImageDataToClipboard.ts +25 -0
- package/src/Clipboard/writeImgBlobToClipboard.ts +13 -0
- package/src/ImageData/{extractImageData.ts → extractImageDataPixels.ts} +21 -3
- package/src/ImageData/imageDataToAlphaMask.ts +35 -0
- package/src/ImageData/imageDataToDataUrl.ts +27 -0
- package/src/ImageData/imageDataToImgBlob.ts +31 -0
- package/src/ImageData/imgBlobToImageData.ts +52 -0
- package/src/ImageData/invertImageData.ts +10 -0
- package/src/ImageData/resizeImageData.ts +75 -0
- package/src/ImageData/{writeImageData.ts → writeImageDataPixels.ts} +22 -3
- package/src/Input/fileInputChangeToImageData.ts +37 -0
- package/src/Input/fileToImageData.ts +75 -0
- package/src/Input/getSupportedRasterFormats.ts +74 -0
- package/src/Mask/extractMask.ts +86 -0
- package/src/Mask/mergeMasks.ts +1 -6
- package/src/PixelData/blendColorPixelData.ts +9 -9
- package/src/PixelData/fillPixelData.ts +51 -12
- package/src/PixelData/invertPixelData.ts +16 -0
- package/src/PixelData/pixelDataToAlphaMask.ts +28 -0
- package/src/Rect/trimRectBounds.ts +118 -0
- package/src/_types.ts +37 -20
- package/src/blend-modes.ts +506 -66
- package/src/color.ts +6 -6
- package/src/globals.d.ts +2 -0
- 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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|