tokimeki-image-editor 0.4.7 → 0.4.11
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/utils/canvas.js
CHANGED
|
@@ -222,21 +222,13 @@ export async function applyTransformWithWebGPU(img, transform, adjustments, crop
|
|
|
222
222
|
if (webgpuCanvas) {
|
|
223
223
|
// Apply stamps and annotations on top (WebGPU doesn't handle these yet)
|
|
224
224
|
if (stampAreas.length > 0 || annotations.length > 0) {
|
|
225
|
-
// Create a new Canvas2D to composite WebGPU result + stamps + annotations
|
|
226
225
|
const finalCanvas = document.createElement('canvas');
|
|
227
226
|
finalCanvas.width = webgpuCanvas.width;
|
|
228
227
|
finalCanvas.height = webgpuCanvas.height;
|
|
229
228
|
const ctx = finalCanvas.getContext('2d');
|
|
230
229
|
if (ctx) {
|
|
231
|
-
// Draw WebGPU result
|
|
232
230
|
ctx.drawImage(webgpuCanvas, 0, 0);
|
|
233
|
-
|
|
234
|
-
const exportViewport = {
|
|
235
|
-
zoom: 1,
|
|
236
|
-
offsetX: 0,
|
|
237
|
-
offsetY: 0,
|
|
238
|
-
scale: 1
|
|
239
|
-
};
|
|
231
|
+
const exportViewport = { zoom: 1, offsetX: 0, offsetY: 0, scale: 1 };
|
|
240
232
|
if (annotations.length > 0) {
|
|
241
233
|
applyAnnotations(finalCanvas, img, exportViewport, annotations, cropArea);
|
|
242
234
|
}
|
|
@@ -358,6 +358,12 @@ export async function exportImage(state) {
|
|
|
358
358
|
const exportCanvas = await applyTransformWithWebGPU(state.imageData.original, state.transform, state.adjustments, state.cropArea, state.blurAreas, state.stampAreas, state.annotations);
|
|
359
359
|
if (_DEV)
|
|
360
360
|
console.timeEnd('[export] 1. WebGPU render');
|
|
361
|
+
// Diagnostic: force GPU readback to measure deferred drawImage cost
|
|
362
|
+
if (_DEV) {
|
|
363
|
+
console.time('[export] 1b. force readback (getImageData)');
|
|
364
|
+
exportCanvas.getContext('2d')?.getImageData(0, 0, 1, 1);
|
|
365
|
+
console.timeEnd('[export] 1b. force readback (getImageData)');
|
|
366
|
+
}
|
|
361
367
|
const format = state.exportOptions.format === 'png' ? 'image/png' : 'image/jpeg';
|
|
362
368
|
if (_DEV)
|
|
363
369
|
console.time('[export] 2. toBlob');
|
|
@@ -1068,7 +1068,10 @@ export async function exportWithWebGPU(imageSource, adjustments, transform, crop
|
|
|
1068
1068
|
console.warn('Failed to get WebGPU context for export');
|
|
1069
1069
|
return null;
|
|
1070
1070
|
}
|
|
1071
|
-
context.configure({
|
|
1071
|
+
context.configure({
|
|
1072
|
+
device, format, alphaMode: 'premultiplied',
|
|
1073
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
|
|
1074
|
+
});
|
|
1072
1075
|
if (_DEV)
|
|
1073
1076
|
console.time('[exportGPU] a. createImageBitmap');
|
|
1074
1077
|
const bitmap = imageSource instanceof ImageBitmap
|
|
@@ -1386,18 +1389,63 @@ export async function exportWithWebGPU(imageSource, adjustments, transform, crop
|
|
|
1386
1389
|
{ binding: 2, resource: { buffer: grainUniformBuffer } },
|
|
1387
1390
|
],
|
|
1388
1391
|
});
|
|
1392
|
+
// Render final pass to canvas AND copy pixels to CPU buffer in the same command
|
|
1393
|
+
const canvasTexture = context.getCurrentTexture();
|
|
1389
1394
|
const finalEncoder = device.createCommandEncoder();
|
|
1390
|
-
|
|
1391
|
-
|
|
1395
|
+
createRenderPass(finalEncoder, canvasTexture.createView(), grainPipeline, grainBindGroup);
|
|
1396
|
+
// Copy rendered pixels directly from GPU texture to a CPU-readable buffer.
|
|
1397
|
+
// This bypasses browser compositor layerization that makes drawImage/toBlob
|
|
1398
|
+
// extremely slow on pages with many compositing layers.
|
|
1399
|
+
const bytesPerRow = Math.ceil(outputWidth * 4 / 256) * 256; // 256-byte alignment required by WebGPU
|
|
1400
|
+
const readBuffer = device.createBuffer({
|
|
1401
|
+
size: bytesPerRow * outputHeight,
|
|
1402
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
1403
|
+
});
|
|
1404
|
+
finalEncoder.copyTextureToBuffer({ texture: canvasTexture }, { buffer: readBuffer, bytesPerRow }, [outputWidth, outputHeight]);
|
|
1392
1405
|
device.queue.submit([finalEncoder.finish()]);
|
|
1393
1406
|
if (_DEV)
|
|
1394
1407
|
console.timeEnd('[exportGPU] b. render passes');
|
|
1395
1408
|
if (_DEV)
|
|
1396
|
-
console.time('[exportGPU] c.
|
|
1397
|
-
await
|
|
1409
|
+
console.time('[exportGPU] c. GPU readback');
|
|
1410
|
+
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
1411
|
+
const gpuData = new Uint8Array(readBuffer.getMappedRange());
|
|
1412
|
+
// Build a pure Canvas2D from the GPU pixel data.
|
|
1413
|
+
// toBlob on this canvas won't trigger compositor layerization.
|
|
1414
|
+
const resultCanvas = document.createElement('canvas');
|
|
1415
|
+
resultCanvas.width = outputWidth;
|
|
1416
|
+
resultCanvas.height = outputHeight;
|
|
1417
|
+
const ctx2d = resultCanvas.getContext('2d');
|
|
1418
|
+
const imageData = ctx2d.createImageData(outputWidth, outputHeight);
|
|
1419
|
+
const dst = new Uint32Array(imageData.data.buffer);
|
|
1420
|
+
const isBGRA = format === 'bgra8unorm';
|
|
1421
|
+
const rowPixels = bytesPerRow / 4;
|
|
1422
|
+
if (isBGRA) {
|
|
1423
|
+
// Swap R↔B channels: BGRA → RGBA
|
|
1424
|
+
const src = new Uint32Array(gpuData.buffer, gpuData.byteOffset, gpuData.byteLength / 4);
|
|
1425
|
+
for (let y = 0; y < outputHeight; y++) {
|
|
1426
|
+
const srcRow = y * rowPixels;
|
|
1427
|
+
const dstRow = y * outputWidth;
|
|
1428
|
+
for (let x = 0; x < outputWidth; x++) {
|
|
1429
|
+
const p = src[srcRow + x];
|
|
1430
|
+
dst[dstRow + x] = (p & 0xFF00FF00) | ((p & 0xFF) << 16) | ((p >> 16) & 0xFF);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
// RGBA — just copy rows (skip padding)
|
|
1436
|
+
const src = new Uint8Array(gpuData.buffer, gpuData.byteOffset, gpuData.byteLength);
|
|
1437
|
+
const dstBytes = imageData.data;
|
|
1438
|
+
const rowBytes = outputWidth * 4;
|
|
1439
|
+
for (let y = 0; y < outputHeight; y++) {
|
|
1440
|
+
dstBytes.set(src.subarray(y * bytesPerRow, y * bytesPerRow + rowBytes), y * rowBytes);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
ctx2d.putImageData(imageData, 0, 0);
|
|
1444
|
+
readBuffer.unmap();
|
|
1398
1445
|
if (_DEV)
|
|
1399
|
-
console.timeEnd('[exportGPU] c.
|
|
1400
|
-
// Cleanup
|
|
1446
|
+
console.timeEnd('[exportGPU] c. GPU readback');
|
|
1447
|
+
// Cleanup
|
|
1448
|
+
readBuffer.destroy();
|
|
1401
1449
|
texture.destroy();
|
|
1402
1450
|
intermediate1.destroy();
|
|
1403
1451
|
intermediate2?.destroy();
|
|
@@ -1411,7 +1459,7 @@ export async function exportWithWebGPU(imageSource, adjustments, transform, crop
|
|
|
1411
1459
|
curveLUTTexture.destroy();
|
|
1412
1460
|
if (_DEV)
|
|
1413
1461
|
console.timeEnd('[exportGPU] total');
|
|
1414
|
-
return
|
|
1462
|
+
return resultCanvas;
|
|
1415
1463
|
}
|
|
1416
1464
|
catch (error) {
|
|
1417
1465
|
console.error('Failed to export with WebGPU:', error);
|