Skip to content

Commit

Permalink
fix: wrong text metrics if text contains chars not including in curre…
Browse files Browse the repository at this point in the history
…nt font-family
  • Loading branch information
Brooooooklyn committed Jun 30, 2022
1 parent bcec165 commit cfcca26
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 108 deletions.
2 changes: 2 additions & 0 deletions __test__/draw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,8 @@ test('measureText', (t) => {
ctx.font = '50px Iosevka Slab'
const metrics = ctx.measureText('@napi-rs/canvas')
t.is(metrics.actualBoundingBoxLeft, -3)
t.is(metrics.actualBoundingBoxAscent, 42)
t.is(metrics.actualBoundingBoxDescent, 10)
t.true(Math.abs(metrics.actualBoundingBoxRight - 372) < 0.001)
})

Expand Down
2 changes: 1 addition & 1 deletion __test__/loadimage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ test('should draw img', async (t) => {
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 250, 250)

await snapshotImage(t, { canvas }, 'jpeg')
await snapshotImage(t, { canvas }, 'jpeg', process.arch === 'x64' ? 0.05 : 0.3)
})
Binary file added __test__/snapshots/should draw img.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added __test__/snapshots/text-align-with-space.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 18 additions & 1 deletion __test__/text.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const test = ava as TestFn<{
}>

const fontIosevka = readFileSync(join(__dirname, 'fonts', 'iosevka-slab-regular.ttf'))

console.assert(GlobalFonts.register(fontIosevka), 'Register Iosevka font failed')

test.beforeEach((t) => {
Expand Down Expand Up @@ -72,3 +71,21 @@ test('text-baseline', async (t) => {
ctx.fillText('abcdefg', 50, 50)
await snapshotImage(t)
})

test('text-align-with-space', async (t) => {
if (process.platform !== 'darwin') {
t.pass('Skip test, no fallback fonts on this platform in CI')
return
}
const { ctx } = t.context
ctx.strokeStyle = 'black'
ctx.lineWidth = 1
ctx.moveTo(100, 0)
ctx.lineTo(100, 512)
ctx.stroke()
ctx.font = '48px sans-serif'
ctx.textAlign = 'center'
ctx.fillText('蒙娜丽莎', 100, 50)
ctx.fillText('兔 宝 宝', 100, 200)
await snapshotImage(t)
})
195 changes: 99 additions & 96 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,107 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/node_modules/canvaskit-wasm/bin/canvaskit.js"></script>
<title>Canvas</title>
</head>

<body>
<style type="text/css">
@font-face {
font-family: 'Iosevka Slab';
src: url('/__test__/fonts/iosevka-slab-regular.ttf');
}
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/node_modules/canvaskit-wasm/bin/canvaskit.js"></script>
<title>Canvas</title>
</head>

body {
font-family: 'Iosevka Slab';
}
<body>
<style type="text/css">
@font-face {
font-family: 'Iosevka Slab';
src: url('/__test__/fonts/iosevka-slab-regular.ttf');
}

canvas {
margin-top: 100px;
}
</style>
<canvas width="1024" height="768" id="canvas"></canvas>
<canvas width="1024" height="768" id="canvas-text"></canvas>
<canvas width="512" height="512" id="regression"></canvas>
<canvas width="512" height="512" id="text"></canvas>
<img width="200" height="200" id="firefox" src="/__test__/fixtures/filter-combine-contrast-brightness.jpeg" />
<script>
setTimeout(() => {
const canvas = document.getElementById('canvas')
/**
* @type {CanvasRenderingContext2D}
*/
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'yellow'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = 'black'
ctx.lineWidth = 3
ctx.font = '50px Iosevka Slab'
body {
font-family: 'Iosevka Slab';
}

const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom']
canvas {
margin-top: 100px;
}
</style>
<canvas width="1024" height="768" id="canvas"></canvas>
<canvas width="1024" height="768" id="canvas-text"></canvas>
<canvas width="512" height="512" id="regression"></canvas>
<canvas width="512" height="512" id="text"></canvas>
<img width="200" height="200" id="firefox" src="/__test__/fixtures/filter-combine-contrast-brightness.jpeg" />
<script>
setTimeout(() => {
const canvas = document.getElementById('canvas')
/**
* @type {CanvasRenderingContext2D}
*/
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'yellow'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = 'black'
ctx.lineWidth = 3
ctx.font = '50px Iosevka Slab'

baselines.forEach(function (baseline) {
ctx.textBaseline = baseline
const metrics = ctx.measureText('@napi-rs/canvas')
console.info(baseline, metrics)
})
const metrics = ctx.measureText('@napi-rs/canvas')
console.info(metrics)
const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom']

ctx.strokeText('skr canvas', 50, 150)
ctx.strokeText('@napi-rs/canvas', 50, 300)
}, 300)
baselines.forEach(function (baseline) {
ctx.textBaseline = baseline
const metrics = ctx.measureText('@napi-rs/canvas')
console.info(baseline, metrics)
})

setTimeout(() => {
const canvas = document.getElementById('canvas-text')
/**
* @type {CanvasRenderingContext2D}
*/
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'yellow'
ctx.fillRect(0, 0, canvas.width, canvas.height)
const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom']
ctx.font = '36px Iosevka Slab'
ctx.strokeStyle = 'red'
ctx.fillStyle = 'black'
ctx.strokeText('skr canvas', 50, 150)
ctx.strokeText('@napi-rs/canvas', 50, 300)
}, 300)

baselines.forEach(function (baseline, index) {
ctx.textBaseline = baseline
const y = 75 + index * 75
ctx.beginPath()
ctx.moveTo(0, y + 0.5)
ctx.lineTo(550, y + 0.5)
ctx.stroke()
ctx.fillText('Abcdefghijklmnop (' + baseline + ')', 0, y)
})
}, 300)
</script>
<script>
setTimeout(() => {
const canvas = document.getElementById('regression')
const ctx = canvas.getContext('2d')
ctx.filter = 'contrast(175%) brightness(103%)'
const image = document.querySelector('#firefox')
ctx.drawImage(image, 0, 0)
}, 1000)
</script>
setTimeout(() => {
const canvas = document.getElementById('canvas-text')
/**
* @type {CanvasRenderingContext2D}
*/
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'yellow'
ctx.fillRect(0, 0, canvas.width, canvas.height)
const baselines = ['top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom']
ctx.font = '50px Iosevka Slab'
ctx.fillStyle = 'black'

baselines.forEach(function (baseline, index) {
ctx.textBaseline = baseline
const y = 75 + index * 75
ctx.beginPath()
ctx.moveTo(0, y + 0.5)
ctx.lineTo(550, y + 0.5)
ctx.stroke()
ctx.fillText('Abcdefghijklmnop (' + baseline + ')', 0, y)
})
}, 300)
</script>
<script>
setTimeout(() => {
const canvas = document.getElementById('regression')
const ctx = canvas.getContext('2d')
ctx.filter = 'contrast(175%) brightness(103%)'
const image = document.querySelector('#firefox')
ctx.drawImage(image, 0, 0)
}, 1000)
</script>

<script>
setTimeout(() => {
const canvas = document.getElementById('text')
const ctx = canvas.getContext('2d')
for (const align of ['center', 'end', 'left', 'right', 'start']) {
const x = canvas.width / 2
ctx.strokeStyle = 'black'
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.stroke()
ctx.textAlign = align
ctx.font = '16px Iosevka Slab'
ctx.fillText('Hello Canvas', x, 200)
}
}, 1000)
</script>
</body>

<script>
setTimeout(() => {
const canvas = document.getElementById('text')
const ctx = canvas.getContext('2d')
for (const align of ['center', 'end', 'left', 'right', 'start']) {
const x = canvas.width / 2
ctx.strokeStyle = 'black'
ctx.moveTo(x, 0)
ctx.lineTo(x, canvas.height)
ctx.stroke()
ctx.textAlign = align
ctx.font = '16px Iosevka Slab'
ctx.fillText('Hello Canvas', x, 200)
}
}, 1000)
</script>
</body>
</html>
</html>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"typescript": "^4.7.2"
},
"lint-staged": {
"*.@(js|ts|tsx|yml|yaml|md|json)": [
"*.@(js|ts|tsx|yml|yaml|md|json|html)": [
"prettier --write"
],
"*.@(js|ts|tsx)": [
Expand Down
23 changes: 14 additions & 9 deletions skia-c/skia_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,23 +383,30 @@ extern "C"
builder.addText(text, text_len);
auto paragraph = static_cast<ParagraphImpl *>(builder.Build().release());
paragraph->layout(MAX_LAYOUT_WIDTH);

std::vector<LineMetrics> metrics_vec;
paragraph->getLineMetrics(metrics_vec);
auto line_metrics = metrics_vec[0];
auto run = paragraph->run(0);
auto glyphs = run.glyphs();
auto font = run.font();
auto glyphs_size = glyphs.size();
SkFontMetrics font_metrics;
font.getMetrics(&font_metrics);
SkRect bounds[glyphs_size];
font.getBounds(glyphs.data(), glyphs_size, &bounds[0], nullptr);
SkRect bounds[text_len];
auto glyphs = run.glyphs();
auto glyphs_size = glyphs.size();
font.getBounds(glyphs.data(), text_len, &bounds[0], nullptr);
auto text_box = paragraph->getRectsForRange(0, text_len, RectHeightStyle::kTight, RectWidthStyle::kTight);
// line_metrics.fWidth doesn't contain the suffix spaces
// run.calculateWidth will return 0 if font is rendering as fallback
auto line_width = 0.0;
auto first_char_bounds = bounds[0];
auto descent = first_char_bounds.fBottom;
auto ascent = first_char_bounds.fTop;
auto last_char_bounds = bounds[glyphs_size - 1];
auto last_char_pos_x = run.positionX(glyphs_size - 1);
for (auto &box : text_box)
{
line_width += box.rect.width();
}
for (size_t i = 1; i <= glyphs_size - 1; ++i)
{
auto char_bounds = bounds[i];
Expand All @@ -414,8 +421,6 @@ extern "C"
ascent = char_top;
}
}
// line_metrics.fWidth doesn't contain the suffix spaces
auto line_width = run.calculateWidth(0, glyphs_size, false);
auto alphabetic_baseline = paragraph->getAlphabeticBaseline();
auto css_baseline = (CssBaseline)baseline;
SkScalar baseline_offset = 0;
Expand Down Expand Up @@ -507,8 +512,8 @@ extern "C"
c_line_metrics->left = line_metrics.fLeft - first_char_bounds.fLeft;
c_line_metrics->right = last_char_pos_x + last_char_bounds.fRight;
c_line_metrics->width = line_width;
c_line_metrics->font_ascent = line_metrics.fAscent + offset;
c_line_metrics->font_descent = line_metrics.fDescent - offset;
c_line_metrics->font_ascent = -font_metrics.fAscent + offset;
c_line_metrics->font_descent = font_metrics.fDescent - offset;
}
delete paragraph;
}
Expand Down

1 comment on commit cfcca26

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: cfcca26 Previous: bcec165 Ratio
Draw house#skia-canvas 18.5 ops/sec (±0.71%) 22.6 ops/sec (±0.05%) 1.22
Draw house#node-canvas 19.4 ops/sec (±0.26%) 26.3 ops/sec (±0.34%) 1.36
Draw house#@napi-rs/skia 19 ops/sec (±0.58%) 22.9 ops/sec (±0.91%) 1.21
Draw gradient#skia-canvas 17.5 ops/sec (±0.8%) 21.7 ops/sec (±0.07%) 1.24
Draw gradient#node-canvas 18.4 ops/sec (±0.46%) 25.1 ops/sec (±0.28%) 1.36
Draw gradient#@napi-rs/skia 17.1 ops/sec (±2.16%) 22.2 ops/sec (±0.08%) 1.30

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.