tokimeki-image-editor 0.4.9 → 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.
@@ -220,41 +220,25 @@ export async function applyTransformWithWebGPU(img, transform, adjustments, crop
220
220
  const { exportWithWebGPU } = await import('./webgpu-render');
221
221
  const webgpuCanvas = await exportWithWebGPU(img, adjustments, transform, cropArea, blurAreas);
222
222
  if (webgpuCanvas) {
223
- // Copy WebGPU result to a Canvas2D canvas.
224
- // Calling toBlob directly on a WebGPU canvas triggers expensive browser
225
- // compositor layerization in complex DOM trees. Canvas2D avoids this.
226
- // Use OffscreenCanvas for the initial blit to bypass the compositor entirely.
227
- const w = webgpuCanvas.width;
228
- const h = webgpuCanvas.height;
229
- let finalCanvas;
230
- if (typeof OffscreenCanvas !== 'undefined') {
231
- // OffscreenCanvas path: completely avoids DOM compositor
232
- const offscreen = new OffscreenCanvas(w, h);
233
- const offCtx = offscreen.getContext('2d');
234
- offCtx.drawImage(webgpuCanvas, 0, 0);
235
- // Transfer to a regular canvas for stamps/annotations (which need DOM canvas APIs)
236
- finalCanvas = document.createElement('canvas');
237
- finalCanvas.width = w;
238
- finalCanvas.height = h;
223
+ // Apply stamps and annotations on top (WebGPU doesn't handle these yet)
224
+ if (stampAreas.length > 0 || annotations.length > 0) {
225
+ const finalCanvas = document.createElement('canvas');
226
+ finalCanvas.width = webgpuCanvas.width;
227
+ finalCanvas.height = webgpuCanvas.height;
239
228
  const ctx = finalCanvas.getContext('2d');
240
- ctx.drawImage(offscreen, 0, 0);
241
- }
242
- else {
243
- finalCanvas = document.createElement('canvas');
244
- finalCanvas.width = w;
245
- finalCanvas.height = h;
246
- const ctx = finalCanvas.getContext('2d');
247
- ctx.drawImage(webgpuCanvas, 0, 0);
248
- }
249
- // Apply stamps and annotations on top
250
- const exportViewport = { zoom: 1, offsetX: 0, offsetY: 0, scale: 1 };
251
- if (annotations.length > 0) {
252
- applyAnnotations(finalCanvas, img, exportViewport, annotations, cropArea);
253
- }
254
- if (stampAreas.length > 0) {
255
- applyStamps(finalCanvas, img, exportViewport, stampAreas, cropArea);
229
+ if (ctx) {
230
+ ctx.drawImage(webgpuCanvas, 0, 0);
231
+ const exportViewport = { zoom: 1, offsetX: 0, offsetY: 0, scale: 1 };
232
+ if (annotations.length > 0) {
233
+ applyAnnotations(finalCanvas, img, exportViewport, annotations, cropArea);
234
+ }
235
+ if (stampAreas.length > 0) {
236
+ applyStamps(finalCanvas, img, exportViewport, stampAreas, cropArea);
237
+ }
238
+ return finalCanvas;
239
+ }
256
240
  }
257
- return finalCanvas;
241
+ return webgpuCanvas;
258
242
  }
259
243
  }
260
244
  catch (error) {
@@ -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.9",
3
+ "version": "0.4.11",
4
4
  "description": "A image editor for svelte.",
5
5
  "type": "module",
6
6
  "license": "MIT",