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.
@@ -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
- // Apply stamps on top
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({ device, format, alphaMode: 'premultiplied' });
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
- const canvasView = context.getCurrentTexture().createView();
1391
- createRenderPass(finalEncoder, canvasView, grainPipeline, grainBindGroup);
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. onSubmittedWorkDone');
1397
- await device.queue.onSubmittedWorkDone();
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. onSubmittedWorkDone');
1400
- // Cleanup per-export resources (pipelines/sampler are cached)
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 canvas;
1462
+ return resultCanvas;
1415
1463
  }
1416
1464
  catch (error) {
1417
1465
  console.error('Failed to export with WebGPU:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokimeki-image-editor",
3
- "version": "0.4.7",
3
+ "version": "0.4.11",
4
4
  "description": "A image editor for svelte.",
5
5
  "type": "module",
6
6
  "license": "MIT",