From 70114ef6debf75207478aa83bd215a5ddffdb6b9 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 18 May 2026 14:00:23 +0300 Subject: [PATCH] fix(io): preserve one-pixel export fringes --- packages/core/src/io/formats/raster/render.ts | 23 ++++++- .../render/canvas/raster-export.test.ts | 60 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/core/src/io/formats/raster/render.ts b/packages/core/src/io/formats/raster/render.ts index 36cb6ac99..d2f01f5e7 100644 --- a/packages/core/src/io/formats/raster/render.ts +++ b/packages/core/src/io/formats/raster/render.ts @@ -80,6 +80,23 @@ function findAlphaBounds(ck: CanvasKit, canvas: Canvas, width: number, height: n return { minX, minY, maxX, maxY } } +const MIN_TRANSPARENT_TRIM_INSET = 2 + +function shouldTrimAlphaBounds( + alphaBounds: NonNullable>, + width: number, + height: number +): boolean { + return ( + Math.max( + alphaBounds.minX, + alphaBounds.minY, + width - alphaBounds.maxX, + height - alphaBounds.maxY + ) >= MIN_TRANSPARENT_TRIM_INSET + ) +} + function renderToSurface( ck: CanvasKit, renderer: SkiaRenderer, @@ -100,7 +117,11 @@ function renderToSurface( setup(canvas) renderer.renderSceneToCanvas(canvas, renderGraph, pageId) surface.flush() - const alphaBounds = trimTransparent ? findAlphaBounds(ck, canvas, width, height) : null + const foundAlphaBounds = trimTransparent ? findAlphaBounds(ck, canvas, width, height) : null + const alphaBounds = + foundAlphaBounds && shouldTrimAlphaBounds(foundAlphaBounds, width, height) + ? foundAlphaBounds + : null const image = alphaBounds ? surface.makeImageSnapshot([ alphaBounds.minX, diff --git a/tests/engine/render/canvas/raster-export.test.ts b/tests/engine/render/canvas/raster-export.test.ts index a53e6ddd0..de792b8cb 100644 --- a/tests/engine/render/canvas/raster-export.test.ts +++ b/tests/engine/render/canvas/raster-export.test.ts @@ -8,11 +8,71 @@ import { expectDefined } from '#tests/helpers/assert' let ck: Awaited> +function rectangleCommandsBlob(x: number, y: number, width: number, height: number): Uint8Array { + const blob = new Uint8Array(1 + 4 * 9 + 1) + const view = new DataView(blob.buffer) + const points = [ + { command: 1, x, y }, + { command: 2, x: x + width, y }, + { command: 2, x: x + width, y: y + height }, + { command: 2, x, y: y + height } + ] + let offset = 0 + for (const point of points) { + blob[offset] = point.command + view.setFloat32(offset + 1, point.x, true) + view.setFloat32(offset + 5, point.y, true) + offset += 9 + } + blob[offset] = 0 + return blob +} + beforeAll(async () => { ck = await initCanvasKit() }) describe('raster export', () => { + test('keeps one-pixel transparent fringes when trimming exports', async () => { + const graph = new SceneGraph() + const page = graph.getPages()[0] + const vector = graph.createNode('VECTOR', page.id, { + width: 10, + height: 10, + fillGeometry: [{ commandsBlob: rectangleCommandsBlob(1, 1, 8, 8) }], + fills: [ + { + type: 'SOLID', + color: { r: 0, g: 0, b: 0, a: 1 }, + opacity: 1, + visible: true + } + ] + }) + + const surface = expectDefined(ck.MakeSurface(1, 1), 'surface') + const renderer = new SkiaRenderer(ck, surface) + + try { + const png = expectDefined( + renderNodesToImage(ck, renderer, graph, page.id, [vector.id], { + scale: 1, + format: 'PNG', + trimTransparent: true + }), + 'png' + ) + const image = expectDefined(ck.MakeImageFromEncoded(png), 'image') + + expect(image.width()).toBe(10) + expect(image.height()).toBe(10) + + image.delete() + } finally { + surface.delete() + } + }) + test('page exports can trim transparent text padding', async () => { const graph = new SceneGraph() const page = graph.getPages()[0]