import { beforeAll, describe, expect, test } from 'bun:test' import { renderNodesToImage, SceneGraph, SkiaRenderer } from '@open-pencil/core' import { initCanvasKit } from '#cli/headless' import { derivedUnderlineRect, shouldUseHardFigmaDerivedGlyphCoverage, snapFigmaDerivedGlyphBaseline } from '#core/canvas/text-derived' import { expectDefined } from '#tests/helpers/assert' let ck: Awaited> beforeAll(async () => { ck = await initCanvasKit() }) function squareCommandsBlob(): Uint8Array { const blob = new Uint8Array(1 + 4 * 9 + 1) const view = new DataView(blob.buffer) let offset = 0 const commands = [ { command: 1, x: 0, y: 0 }, { command: 2, x: 1, y: 0 }, { command: 2, x: 1, y: 1 }, { command: 2, x: 0, y: 1 } ] for (const { command, x, y } of commands) { blob[offset] = command view.setFloat32(offset + 1, x, true) view.setFloat32(offset + 5, y, true) offset += 9 } blob[offset] = 0 return blob } describe('derived text rendering', () => { test('snaps Figma glyph baselines to device pixels', () => { expect(snapFigmaDerivedGlyphBaseline(47.45454406738281)).toBe(47) expect(snapFigmaDerivedGlyphBaseline(15.090909004211426)).toBe(15) expect(snapFigmaDerivedGlyphBaseline(17.81818199157715)).toBe(18) }) test('uses hard source coverage only for regular 20px derived glyphs', () => { expect(shouldUseHardFigmaDerivedGlyphCoverage({ fontSize: 20, fontWeight: 400 })).toBeTrue() expect(shouldUseHardFigmaDerivedGlyphCoverage({ fontSize: 20, fontWeight: 600 })).toBeFalse() expect(shouldUseHardFigmaDerivedGlyphCoverage({ fontSize: 14, fontWeight: 500 })).toBeFalse() }) test('places derived underlines at Figma-like subpixel coverage bounds', () => { expect(derivedUnderlineRect({ width: 70 }, 17)).toEqual({ x1: 0, y1: 19.75, x2: 69.25, y2: 20.75 }) }) test('draws Figma-derived glyphs even when the font is unavailable', async () => { const graph = new SceneGraph() const page = graph.getPages()[0] const text = graph.createNode('TEXT', page.id, { width: 20, height: 20, text: 'x', fontFamily: '__MissingFont__', fills: [ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } ], figmaDerivedTextGlyphs: [ { commandsBlob: squareCommandsBlob(), x: 2, y: 12, fontSize: 10 } ] }) const surface = expectDefined(ck.MakeSurface(1, 1), 'surface') const renderer = new SkiaRenderer(ck, surface) try { const png = expectDefined( renderNodesToImage(ck, renderer, graph, page.id, [text.id], { scale: 1, format: 'PNG' }), 'png' ) const image = expectDefined(ck.MakeImageFromEncoded(png), 'image') const pixels = expectDefined( image.readPixels(0, 0, { alphaType: ck.AlphaType.Unpremul, colorType: ck.ColorType.RGBA_8888, colorSpace: ck.ColorSpace.SRGB, width: image.width(), height: image.height() }), 'pixels' ) expect(Math.max(...pixels.filter((_, index) => index % 4 === 3))).toBeGreaterThan(0) image.delete() } finally { surface.delete() } }) test('draws underlines for Figma-derived text decorations', async () => { const graph = new SceneGraph() const page = graph.getPages()[0] const text = graph.createNode('TEXT', page.id, { width: 20, height: 24, text: 'x', fontFamily: '__MissingFont__', textDecoration: 'UNDERLINE', fills: [ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } ], figmaDerivedTextGlyphs: [ { commandsBlob: squareCommandsBlob(), x: 2, y: 12, fontSize: 10 } ] }) const surface = expectDefined(ck.MakeSurface(1, 1), 'surface') const renderer = new SkiaRenderer(ck, surface) try { const png = expectDefined( renderNodesToImage(ck, renderer, graph, page.id, [text.id], { scale: 1, format: 'PNG' }), 'png' ) const image = expectDefined(ck.MakeImageFromEncoded(png), 'image') const pixels = expectDefined( image.readPixels(0, 0, { alphaType: ck.AlphaType.Unpremul, colorType: ck.ColorType.RGBA_8888, colorSpace: ck.ColorSpace.SRGB, width: image.width(), height: image.height() }), 'pixels' ) const underlinedRows = [15, 16].map((y) => pixels[(y * image.width() + 8) * 4 + 3]) expect(Math.max(...underlinedRows)).toBeGreaterThan(0) image.delete() } finally { surface.delete() } }) test('does not draw base derived underlines through NONE style runs', async () => { const graph = new SceneGraph() const page = graph.getPages()[0] const text = graph.createNode('TEXT', page.id, { width: 60, height: 28, text: 'on off', fontFamily: '__MissingFont__', textDecoration: 'UNDERLINE', styleRuns: [ { start: 3, length: 3, style: { textDecoration: 'NONE' } } ], fills: [ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } ], figmaDerivedTextGlyphs: [ { commandsBlob: squareCommandsBlob(), x: 2, y: 12, fontSize: 10 } ] }) const surface = expectDefined(ck.MakeSurface(1, 1), 'surface') const renderer = new SkiaRenderer(ck, surface) try { const png = expectDefined( renderNodesToImage(ck, renderer, graph, page.id, [text.id], { scale: 1, format: 'PNG' }), 'png' ) const image = expectDefined(ck.MakeImageFromEncoded(png), 'image') const pixels = expectDefined( image.readPixels(0, 0, { alphaType: ck.AlphaType.Unpremul, colorType: ck.ColorType.RGBA_8888, colorSpace: ck.ColorSpace.SRGB, width: image.width(), height: image.height() }), 'pixels' ) const leftUnderlineAlpha = Math.max( pixels[(15 * image.width() + 12) * 4 + 3] ?? 0, pixels[(16 * image.width() + 12) * 4 + 3] ?? 0 ) const disabledUnderlineAlpha = Math.max( pixels[(15 * image.width() + 45) * 4 + 3] ?? 0, pixels[(16 * image.width() + 45) * 4 + 3] ?? 0 ) expect(leftUnderlineAlpha).toBeGreaterThan(0) expect(disabledUnderlineAlpha).toBe(0) image.delete() } finally { surface.delete() } }) test('draws styled decoration runs for Figma-derived text', async () => { const graph = new SceneGraph() const page = graph.getPages()[0] const text = graph.createNode('TEXT', page.id, { width: 60, height: 28, text: 'red blue', fontFamily: '__MissingFont__', textDecoration: 'UNDERLINE', textDecorationStyle: 'WAVY', textDecorationThickness: 2, textDecorationFills: [ { type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } ], styleRuns: [ { start: 4, length: 4, style: { textDecorationStyle: 'DOTTED', textDecorationThickness: 3, textDecorationFills: [ { type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true } ] } } ], fills: [ { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } ], figmaDerivedTextGlyphs: [ { commandsBlob: squareCommandsBlob(), x: 2, y: 12, fontSize: 10 } ] }) const surface = expectDefined(ck.MakeSurface(1, 1), 'surface') const renderer = new SkiaRenderer(ck, surface) try { const png = expectDefined( renderNodesToImage(ck, renderer, graph, page.id, [text.id], { scale: 1, format: 'PNG' }), 'png' ) const image = expectDefined(ck.MakeImageFromEncoded(png), 'image') const pixels = expectDefined( image.readPixels(0, 0, { alphaType: ck.AlphaType.Unpremul, colorType: ck.ColorType.RGBA_8888, colorSpace: ck.ColorSpace.SRGB, width: image.width(), height: image.height() }), 'pixels' ) let redPixels = 0 let bluePixels = 0 for (let index = 0; index < pixels.length; index += 4) { const red = pixels[index] ?? 0 const green = pixels[index + 1] ?? 0 const blue = pixels[index + 2] ?? 0 const alpha = pixels[index + 3] ?? 0 if (alpha > 0 && red > 150 && green < 80 && blue < 80) redPixels++ if (alpha > 0 && blue > 150 && red < 80 && green < 80) bluePixels++ } expect(redPixels).toBeGreaterThan(0) expect(bluePixels).toBeGreaterThan(0) image.delete() } finally { surface.delete() } }) })