chromium/third_party/blink/web_tests/fast/canvas-api/textMetrics.html

<!DOCTYPE HTML>
<head>
<meta charset="UTF-8">
<style>
@font-face {
  font-family: Libertine;
  src: url('../../third_party/Libertine/LinLibertine_R.woff');
}
</style>
</head>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>

function getMetric(ctx, text, baseline, align, rtl, metric) {
  ctx.font = '50px Libertine';
  ctx.textBaseline = baseline;
  ctx.textAlign = align;
  ctx.direction = rtl ? "rtl": "ltr";

  const tm = ctx.measureText(text);
  tm.actualBoundingBoxWidth = Math.abs(tm.actualBoundingBoxRight + tm.actualBoundingBoxLeft);
  tm.actualBoundingBoxHeight = Math.abs(tm.actualBoundingBoxDescent + tm.actualBoundingBoxAscent);
  if (["alphabetic", "ideographic", "hanging"].includes(metric)) {
    var bl = metric + "Baseline";
    return tm[bl];
  } else {
    return tm[metric];
  }
}

function testMetric(name, ctx, text, baseline, align, rtl, metric, expected_value, epsilon = 0.3) {
  const value = getMetric(ctx, text, baseline, align, rtl, metric);
  assert_approx_equals(value, expected_value, Math.ceil(Math.abs(epsilon * expected_value)) + 10,
    name + ": " + metric + " for '" + text + "' (" + baseline + ", " + align + ")");
  return value;
}

const kAligns = [ "left", "right", "center", "start", "end" ];
const kBaselines =  ['top','hanging','middle','alphabetic',
                     'ideographic','bottom'];

// Those bbox are given from alphabetic,left.
// bbox = [ left, right, ascent, descent ].
const kTexts = [
  { text: "", width: 0, bbox: [0, 0, 0, 0], rtl: false },
  { text: "hello", width: 100, bbox: [0, 99, 35, 1], rtl: false },
  { text: "gypsy", width: 122, bbox: [-1, 122, 23, 12], rtl: false },
  { text: "傳統是假的", width: 250, bbox: [-1, 247, 43, 5], rtl: false },
  { text: "フェミニズム", width: 300, bbox: [-7, 297, 43, 3], rtl: false },
  { text: "एक आम भाषा", width: 250, bbox: [1, 251, 35, 7], rtl: false },
  { text: "ليس في اسمنا", width: 301, bbox: [-4, 297, 38, 13], rtl: true },
]

function forEachExample(fn) {
  for (const ex of kTexts) {
    for (const bline of kBaselines) {
      for (const align of kAligns) {
        fn(ex, bline, align);
      }
    }
  }
}

// Widths shouldn't change over baseline/align.
function testWidth(ctx) {
  forEachExample((ex, bline, align) => {
    testMetric("width", ctx, ex.text, bline, align, ex.rtl, "width", ex.width);
  });
}

// Reality check that the bounding box we are using for input
// match the returned values with a big confidence interval.
// If it does, we adapt to the actual values to accomodate for different OSes.
function testAndFixBasicExpectations(ctx) {
  for (const ex of kTexts) {
    ex.bbox[0] = testMetric("expectations", ctx, ex.text, "alphabetic", "left", ex.rtl, "actualBoundingBoxLeft", ex.bbox[0]);
    ex.bbox[1] = testMetric("expectations", ctx, ex.text, "alphabetic", "left", ex.rtl, "actualBoundingBoxRight", ex.bbox[1]);
    ex.bbox[2] = testMetric("expectations", ctx, ex.text, "alphabetic", "left", ex.rtl, "actualBoundingBoxAscent", ex.bbox[2]);
    ex.bbox[3] = testMetric("expectations", ctx, ex.text, "alphabetic", "left", ex.rtl, "actualBoundingBoxDescent", ex.bbox[3]);
  }
}


function testBaselines(ctx) {
  // on their own baseline, metrics should be 0.
  for (const ex of kTexts) {
    for (const bline of ["alphabetic", "ideographic", "hanging"]) {
      for (const align of kAligns) {
        testMetric("own baselines", ctx, ex.text, bline, align, ex.rtl, bline, 0);
      }
    }
  }

  // Those 3 baselines are font size dependent, so everything should be fixed.
  forEachExample((ex, _, align) => {
    testMetric("top baseline", ctx, ex.text, "top", align, ex.rtl, "alphabetic", -44);
  });

  forEachExample((ex, _, align) => {
    testMetric("middle baseline", ctx, ex.text, "middle", align, ex.rtl, "alphabetic", -15);
  });

  forEachExample((ex, _, align) => {
    testMetric("bottom baseline", ctx, ex.text, "bottom", align, ex.rtl, "alphabetic", 13);
  });
}

function testBoundingBox(ctx) {
  // Width/Height should remain the same across baseline/align.
  forEachExample((ex, bline, align) => {
    testMetric("boundingbox", ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxWidth",
      ex.bbox[1] + ex.bbox[0]);
    testMetric("boundingbox", ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxHeight",
      ex.bbox[2] + ex.bbox[3]);
  });

  // Left changes depending on what is the alignment.
  forEachExample((ex, bline, align) => {
    let left = ex.bbox[0];
    let calign = align;
    if (align == "start") calign = ex.rtl ? "right" : "left";
    if (align == "end") calign = ex.rtl ? "left" : "right";

    if (calign == "left") left = ex.bbox[0];
    else if (calign == "right") left = ex.bbox[0] + ex.width
    else if (calign == "center") left = (ex.bbox[0] + ex.width)/2.0;

    testMetric("boundigbox alignment", ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxLeft", left);
  });

  // Right changes depending on what is the alignment.
  forEachExample((ex, bline, align) => {
    let right = ex.bbox[0];
    let calign = align;
    if (align == "start") calign = ex.rtl ? "right" : "left";
    if (align == "end") calign = ex.rtl ? "left" : "right";

    if (calign == "left") right = ex.bbox[0] + ex.width;
    else if (calign == "right") right = ex.bbox[0];
    else if (calign == "center") right = (ex.bbox[0] + ex.width)/2.0;

    testMetric("boundingbox alignment", ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxRight", right);
  });
}

// fontAscent/Descent always includes actual.
function testFontAscentDescent(ctx) {
  forEachExample((ex, bline, align) => {
    const font_asc = getMetric(ctx, ex.text, bline, align, ex.rtl, "fontBoundingBoxAscent");
    const font_des = getMetric(ctx, ex.text, bline, align, ex.rtl, "fontBoundingBoxAscent");
    const actual_asc = getMetric(ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxAscent");
    const actual_des = getMetric(ctx, ex.text, bline, align, ex.rtl, "actualBoundingBoxAscent");

    assert_greater_than(font_asc, actual_asc, "font_asc > actual_asc for '" + ex.text + "'");
    assert_greater_than(font_des + font_asc, actual_asc + actual_des, "font_end > actal_end '" + ex.text + "'");
  });
}

function measureMetrics(ctx) {
  testAndFixBasicExpectations(ctx);
  testWidth(ctx);
  testBaselines(ctx);
  testBoundingBox(ctx);
  testFontAscentDescent(ctx);
  // TODO(fserb): missing hanging/ideographic baseline tests.
  // TODO(fserb): missing emHeight tests.
}

async_test(t => {
    var canvas = document.createElement('canvas');
    canvas.width = 100;
    canvas.height = 100;
    var ctx = canvas.getContext('2d');
    ctx.font = '50px Libertine';
    // Kick off loading of the font
    ctx.fillText(" ", 0, 0);
    document.fonts.addEventListener('loadingdone', t.step_func_done(function() {
        measureMetrics(ctx);
    }));
}, "Test all attributes of TextMetrics.");
</script>