Skip to content

Commit

Permalink
Merge pull request #985 from zbjornson/983-baselines
Browse files Browse the repository at this point in the history
Fix text baselines and measureText
  • Loading branch information
LinusU committed Sep 2, 2017
2 parents 4854f0f + ec4871f commit 28a063a
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 43 deletions.
67 changes: 30 additions & 37 deletions src/CanvasRenderingContext2d.cc
Expand Up @@ -1926,14 +1926,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 @@ -1954,22 +1972,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 @@ -2090,20 +2095,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 @@ -2112,16 +2106,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 @@ -747,13 +747,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 @@ -2174,3 +2174,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)
}

0 comments on commit 28a063a

Please sign in to comment.