Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix text baselines and measureText #985

Merged
merged 1 commit into from Sep 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 30 additions & 37 deletions src/CanvasRenderingContext2d.cc
Expand Up @@ -1927,14 +1927,32 @@ NAN_METHOD(Context2d::StrokeText) {
cairo_scale(context->context(), 1 / scaled_by, 1);
}

/*
* Gets the baseline adjustment in device pixels, taking into account the
* transformation matrix. TODO This does not handle skew (which cannot easily
* be extracted from the matrix separately from rotation).
*/
inline double getBaselineAdjustment(PangoFontMetrics* metrics, cairo_matrix_t matrix, short baseline) {
double yScale = sqrt(matrix.yx * matrix.yx + matrix.yy * matrix.yy);
switch (baseline) {
case TEXT_BASELINE_ALPHABETIC:
return (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * yScale;
case TEXT_BASELINE_MIDDLE:
return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / (2.0 * PANGO_SCALE)) * yScale;
case TEXT_BASELINE_BOTTOM:
return ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * yScale;
default:
return 0;
}
}

/*
* Set text path for the given string at (x, y).
*/

void
Context2d::setTextPath(const char *str, double x, double y) {
PangoRectangle ink_rect, logical_rect;
PangoFontMetrics *metrics = NULL;
cairo_matrix_t matrix;

pango_layout_set_text(_layout, str, -1);
Expand All @@ -1955,22 +1973,9 @@ Context2d::setTextPath(const char *str, double x, double y) {
break;
}

switch (state->textBaseline) {
case TEXT_BASELINE_ALPHABETIC:
metrics = PANGO_LAYOUT_GET_METRICS(_layout);
y -= (pango_font_metrics_get_ascent(metrics) / PANGO_SCALE) * matrix.yy;
break;
case TEXT_BASELINE_MIDDLE:
metrics = PANGO_LAYOUT_GET_METRICS(_layout);
y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE)) * matrix.yy;
break;
case TEXT_BASELINE_BOTTOM:
metrics = PANGO_LAYOUT_GET_METRICS(_layout);
y -= ((pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE) * matrix.yy;
break;
}

if (metrics) pango_font_metrics_unref(metrics);
PangoFontMetrics *metrics = PANGO_LAYOUT_GET_METRICS(_layout);
y -= getBaselineAdjustment(metrics, matrix, state->textBaseline);
pango_font_metrics_unref(metrics);

cairo_move_to(_context, x, y);
if (state->textDrawingMode == TEXT_DRAW_PATHS) {
Expand Down Expand Up @@ -2091,20 +2096,9 @@ NAN_METHOD(Context2d::MeasureText) {
x_offset = 0.0;
}

double y_offset;
switch (context->state->textBaseline) {
case TEXT_BASELINE_ALPHABETIC:
y_offset = -pango_font_metrics_get_ascent(metrics) / PANGO_SCALE;
break;
case TEXT_BASELINE_MIDDLE:
y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics))/(2.0 * PANGO_SCALE);
break;
case TEXT_BASELINE_BOTTOM:
y_offset = -(pango_font_metrics_get_ascent(metrics) + pango_font_metrics_get_descent(metrics)) / PANGO_SCALE;
break;
default:
y_offset = 0.0;
}
cairo_matrix_t matrix;
cairo_get_matrix(ctx, &matrix);
double y_offset = getBaselineAdjustment(metrics, matrix, context->state->textBaseline);

obj->Set(Nan::New<String>("width").ToLocalChecked(),
Nan::New<Number>(logical_rect.width));
Expand All @@ -2113,16 +2107,15 @@ NAN_METHOD(Context2d::MeasureText) {
obj->Set(Nan::New<String>("actualBoundingBoxRight").ToLocalChecked(),
Nan::New<Number>(x_offset + PANGO_RBEARING(logical_rect)));
obj->Set(Nan::New<String>("actualBoundingBoxAscent").ToLocalChecked(),
Nan::New<Number>(-(y_offset+ink_rect.y)));
Nan::New<Number>(y_offset + PANGO_ASCENT(ink_rect)));
obj->Set(Nan::New<String>("actualBoundingBoxDescent").ToLocalChecked(),
Nan::New<Number>((PANGO_DESCENT(ink_rect) + y_offset)));
Nan::New<Number>(PANGO_DESCENT(ink_rect) - y_offset));
obj->Set(Nan::New<String>("emHeightAscent").ToLocalChecked(),
Nan::New<Number>(PANGO_ASCENT(logical_rect) - y_offset));
Nan::New<Number>(-(PANGO_ASCENT(logical_rect) - y_offset)));
obj->Set(Nan::New<String>("emHeightDescent").ToLocalChecked(),
Nan::New<Number>(PANGO_DESCENT(logical_rect) + y_offset));
Nan::New<Number>(PANGO_DESCENT(logical_rect) - y_offset));
obj->Set(Nan::New<String>("alphabeticBaseline").ToLocalChecked(),
Nan::New<Number>((pango_font_metrics_get_ascent(metrics) / PANGO_SCALE)
+ y_offset));
Nan::New<Number>(-(pango_font_metrics_get_ascent(metrics) / PANGO_SCALE - y_offset)));

pango_font_metrics_unref(metrics);

Expand Down
36 changes: 30 additions & 6 deletions test/canvas.test.js
Expand Up @@ -746,13 +746,37 @@ describe('Canvas', function () {
});
});

it('Context2d#measureText().width', function () {
var canvas = createCanvas(20, 20)
, ctx = canvas.getContext('2d');
describe('Context2d#measureText()', function () {
it('Context2d#measureText().width', function () {
var canvas = createCanvas(20, 20)
, ctx = canvas.getContext('2d');

assert.ok(ctx.measureText('foo').width);
assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width);
assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width);
assert.ok(ctx.measureText('foo').width);
assert.ok(ctx.measureText('foo').width != ctx.measureText('foobar').width);
assert.ok(ctx.measureText('foo').width != ctx.measureText(' foo').width);
});

it('works', function () {
var canvas = createCanvas(20, 20)
var ctx = canvas.getContext('2d')
ctx.font = "20px Arial"

ctx.textBaseline = "alphabetic"
var metrics = ctx.measureText("Alphabet")
// Zero if the given baseline is the alphabetic baseline
assert.equal(metrics.alphabeticBaseline, 0)
// Positive = going up from the baseline
assert.ok(metrics.actualBoundingBoxAscent > 0)
// Positive = going down from the baseline
assert.ok(metrics.actualBoundingBoxDescent > 0) // ~4-5

ctx.textBaseline = "bottom"
metrics = ctx.measureText("Alphabet")
assert.ok(metrics.alphabeticBaseline > 0) // ~4-5
assert.ok(metrics.actualBoundingBoxAscent > 0)
// On the baseline or slightly above
assert.ok(metrics.actualBoundingBoxDescent <= 0)
});
});

it('Context2d#createImageData(ImageData)', function () {
Expand Down
92 changes: 92 additions & 0 deletions test/public/tests.js
Expand Up @@ -2163,3 +2163,95 @@ tests['textBaseline and scale'] = function (ctx) {
ctx.textAlign = 'center'
ctx.fillText('bottom', 1000, 1500)
}

tests['rotated baseline'] = function (ctx) {
ctx.font = '12px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
ctx.textBaseline = 'bottom'
ctx.translate(100, 100)

for (var i = 0; i < 16; i++) {
ctx.fillText('Hello world!', -50, -50)
ctx.rotate(-Math.PI / 8)
}
}

tests['rotated and scaled baseline'] = function (ctx) {
ctx.font = '120px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
ctx.textBaseline = 'bottom'
ctx.translate(100, 100)
ctx.scale(0.1, 0.2)

for (var i = 0; i < 16; i++) {
ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2)
ctx.rotate(-Math.PI / 8)
}
}

tests['rotated and skewed baseline'] = function (ctx) {
ctx.font = '12px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
ctx.textBaseline = 'bottom'
ctx.translate(100, 100)
ctx.transform(1, 1, 0, 1, 1, 1)

for (var i = 0; i < 16; i++) {
ctx.fillText('Hello world!', -50, -50)
ctx.rotate(-Math.PI / 8)
}
}

tests['rotated, scaled and skewed baseline'] = function (ctx) {
// Known issue: we don't have a way to decompose the cairo matrix into the
// skew and rotation separately.
ctx.font = '120px Arial'
ctx.fillStyle = 'black'
ctx.textAlign = 'center'
ctx.textBaseline = 'bottom'
ctx.translate(100, 100)
ctx.scale(0.1, 0.2)
ctx.transform(1, 1, 0, 1, 1, 1)

for (var i = 0; i < 16; i++) {
ctx.fillText('Hello world!', -50 / 0.1, -50 / 0.2)
ctx.rotate(-Math.PI / 8)
}
}

tests['measureText()'] = function (ctx) {
// Note: As of Sep 2017, Chrome is the only browser with advanced TextMetrics,
// and they're behind a flag, and a few of them are missing and others are
// wrong.
function drawWithBBox (text, x, y) {
ctx.fillText(text, x, y)
ctx.strokeStyle = 'red'
ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(200, y + 0.5); ctx.stroke()
var metrics = ctx.measureText(text)
ctx.strokeStyle = 'blue'
ctx.strokeRect(
x - metrics.actualBoundingBoxLeft + 0.5,
y - metrics.actualBoundingBoxAscent + 0.5,
metrics.width,
metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
)
}

ctx.font = '20px Arial'
ctx.textBaseline = 'alphabetic'
drawWithBBox('Alphabet alphabetic', 20, 50)

drawWithBBox('weruoasnm', 50, 175) // no ascenders/descenders

drawWithBBox(',', 100, 125) // tiny height

ctx.textBaseline = 'bottom'
drawWithBBox('Alphabet bottom', 20, 90)

ctx.textBaseline = 'alphabetic'
ctx.rotate(Math.PI / 8)
drawWithBBox('Alphabet', 50, 100)
}