Use a test-owned family with bundled Regular and SemiBold faces so the nonblank raster oracle does not depend on earlier tests loading Inter 600. Keep exact pixel equality and assert font readiness before painting.
126 lines
4.1 KiB
TypeScript
126 lines
4.1 KiB
TypeScript
import { beforeAll, expect, test } from 'bun:test'
|
|
|
|
import type { Paragraph } from 'canvaskit-wasm'
|
|
|
|
import type { Fill } from '@open-pencil/scene-graph'
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
|
|
import { initCanvasKit } from '#cli/headless'
|
|
import { SkiaRenderer } from '#core/canvas/renderer'
|
|
import { withTextParagraph } from '#core/canvas/text'
|
|
import { fontManager } from '#core/text/fonts'
|
|
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
import { repoPath } from '#tests/helpers/paths'
|
|
|
|
const FONT_FAMILY = 'Painted text fixture'
|
|
beforeAll(async () => {
|
|
for (const style of ['Regular', 'SemiBold']) {
|
|
const data = await Bun.file(repoPath(`public/Inter-${style}.ttf`)).arrayBuffer()
|
|
fontManager.markLoaded(FONT_FAMILY, style, data)
|
|
}
|
|
})
|
|
|
|
const solid: Fill = { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }
|
|
const gradient: Fill = {
|
|
...solid,
|
|
type: 'GRADIENT_LINEAR',
|
|
gradientStops: [
|
|
{ position: 0, color: solid.color },
|
|
{ position: 1, color: solid.color }
|
|
],
|
|
gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
}
|
|
|
|
test('mutable foreground paints never enter the paragraph cache, including on errors', async () => {
|
|
const ck = await initCanvasKit()
|
|
const renderer = new SkiaRenderer(ck, expectDefined(ck.MakeSurface(100, 100), 'surface'))
|
|
const graph = new SceneGraph()
|
|
const page = expectDefined(graph.getPages()[0], 'page')
|
|
const node = graph.createNode('TEXT', page.id, {
|
|
text: 'Paint',
|
|
fontFamily: FONT_FAMILY,
|
|
width: 100,
|
|
height: 100
|
|
})
|
|
let borrowed: Paragraph | undefined
|
|
try {
|
|
await renderer.loadFonts()
|
|
expect(() =>
|
|
withTextParagraph(
|
|
renderer,
|
|
node,
|
|
ck.BLACK,
|
|
{ halfLeading: true, foregroundPaint: renderer.fillPaint },
|
|
(paragraph) => {
|
|
borrowed = paragraph
|
|
throw new Error('paint failed')
|
|
}
|
|
)
|
|
).toThrow('paint failed')
|
|
expect(borrowed?.isDeleted()).toBe(true)
|
|
} finally {
|
|
renderer.destroy()
|
|
}
|
|
})
|
|
|
|
for (const textAlignVertical of ['TOP', 'CENTER', 'BOTTOM'] as const) {
|
|
for (const fixture of [
|
|
{ text: 'OPEN', fontSize: 88, width: 560, height: 96 },
|
|
{ text: 'office AV\nOPEN', fontSize: 32, width: 130, height: 180 },
|
|
{
|
|
text: 'Bold AV\noffice',
|
|
fontSize: 32,
|
|
width: 130,
|
|
height: 180,
|
|
styleRuns: [{ start: 0, length: 4, style: { fontSize: 40, fontWeight: 600 } }]
|
|
}
|
|
]) {
|
|
test(`complex paint preserves paragraph coverage: ${textAlignVertical} ${fixture.text}`, async () => {
|
|
const ck = await initCanvasKit()
|
|
const graph = new SceneGraph()
|
|
const page = expectDefined(graph.getPages()[0], 'page')
|
|
const node = graph.createNode('TEXT', page.id, {
|
|
...fixture,
|
|
fontFamily: FONT_FAMILY,
|
|
fontWeight: 400,
|
|
textAlignVertical,
|
|
textAutoResize: 'NONE',
|
|
fills: [solid]
|
|
})
|
|
const surface = expectDefined(ck.MakeSurface(fixture.width, fixture.height), 'surface')
|
|
const renderer = new SkiaRenderer(ck, surface)
|
|
try {
|
|
await renderer.loadFonts()
|
|
expect(renderer.nodeFontReadiness(node)).toBe('ready')
|
|
const draw = (fill: Fill) => {
|
|
graph.updateNode(node.id, { fills: [fill] })
|
|
const canvas = surface.getCanvas()
|
|
canvas.clear(ck.WHITE)
|
|
renderer.renderShape(canvas, node, graph)
|
|
surface.flush()
|
|
const image = surface.makeImageSnapshot()
|
|
try {
|
|
const pixels = image.readPixels(0, 0, {
|
|
width: fixture.width,
|
|
height: fixture.height,
|
|
colorType: ck.ColorType.RGBA_8888,
|
|
alphaType: ck.AlphaType.Unpremul,
|
|
colorSpace: ck.ColorSpace.SRGB
|
|
})
|
|
if (!(pixels instanceof Uint8Array)) throw new Error('RGBA bytes unavailable')
|
|
return pixels.slice()
|
|
} finally {
|
|
image.delete()
|
|
}
|
|
}
|
|
const reference = draw(solid)
|
|
expect(reference.some((value, index) => index % 4 !== 3 && value < 128)).toBe(true)
|
|
expect(draw(gradient)).toEqual(reference)
|
|
} finally {
|
|
renderer.destroy()
|
|
}
|
|
})
|
|
}
|
|
}
|