<commit_before>/**
 * Copyright 2020 <NAME> <<EMAIL>>
 */

import { MonospaceFontMetrics } from "./MonospaceFontMetrics";
import { Logger, getLogger, log } from "extraterm-logging";

const computeFontMetricsCache = new Map<string, MonospaceFontMetrics>();
const computeDpiFontMetricsCache = new Map<string, { renderFontMetrics: MonospaceFontMetrics, cssFontMetrics: MonospaceFontMetrics }>();


export function computeFontMetrics(fontFamily: string, fontSizePx: number, sampleChars: string[]=null): MonospaceFontMetrics {
  const cacheKey = `${fontFamily};${fontSizePx};${sampleChars == null ? "" : sampleChars.join("")}`;

  let metrics = computeFontMetricsCache.get(cacheKey);
  if (metrics != null) {
    return metrics;
  }

  const fm = new FontMeasurement();
  metrics = fm.computeFontMetrics({ family: fontFamily, sizePx: fontSizePx, sampleChars });

  computeFontMetricsCache.set(cacheKey, metrics);
  return metrics;
}

export function computeDpiFontMetrics(fontFamily: string, fontSizePx: number, devicePixelRatio: number,
    sampleChars: string[]=null): { renderFontMetrics: MonospaceFontMetrics, cssFontMetrics: MonospaceFontMetrics } {

  const cacheKey = `${fontFamily};${fontSizePx};${devicePixelRatio};${sampleChars == null ? "" : sampleChars.join("")}`;
  let metrics = computeDpiFontMetricsCache.get(cacheKey);
  if (metrics != null) {
    return metrics;
  }

  const fm = new FontMeasurement();
  const renderFontSizePx = fontSizePx * devicePixelRatio;
  const renderFontMetrics = fm.computeFontMetrics({ family: fontFamily, sizePx: renderFontSizePx, sampleChars });

  const cssFontMetrics: MonospaceFontMetrics = {
    fontSizePx: renderFontMetrics.fontSizePx / devicePixelRatio,
    fontFamily,

    fillTextYOffset: renderFontMetrics.fillTextYOffset / devicePixelRatio,
    fillTextXOffset: renderFontMetrics.fillTextYOffset / devicePixelRatio,

    widthPx: renderFontMetrics.widthPx / devicePixelRatio,
    heightPx: renderFontMetrics.heightPx / devicePixelRatio,
    boldItalicWidthPx: renderFontMetrics.boldItalicWidthPx / devicePixelRatio,

    strikethroughY: renderFontMetrics.strikethroughY / devicePixelRatio,
    strikethroughHeight: renderFontMetrics.strikethroughHeight / devicePixelRatio,
    underlineY: renderFontMetrics.underlineY / devicePixelRatio,
    secondUnderlineY: renderFontMetrics.secondUnderlineY / devicePixelRatio,
    underlineHeight: renderFontMetrics.underlineHeight / devicePixelRatio,

    overlineY: renderFontMetrics.overlineY / devicePixelRatio,
    overlineHeight: renderFontMetrics.overlineHeight / devicePixelRatio,

    curlyHeight: renderFontMetrics.curlyHeight / devicePixelRatio,
    curlyThickness: renderFontMetrics.curlyThickness / devicePixelRatio,
    curlyY: renderFontMetrics.curlyY / devicePixelRatio,
  };

  metrics = {
    renderFontMetrics,
    cssFontMetrics,
  };

  computeDpiFontMetricsCache.set(cacheKey, metrics);
  return metrics;
}

interface TextMeasurements {
  topY: number;
  bottomY: number;
  leftX: number;
  rightX: number;
}

interface ComputeFontMetricsOptions {
  family: string;
  sizePx: number;
  sampleChars?: string[];
}

class FontMeasurement {
  private _log: Logger = null;
  private _canvasWidthPx = 250;
  private _canvasHeightPx = 250;

  constructor() {
    this._log = getLogger("FontMeasurement", this);
  }

  computeFontMetrics(options: ComputeFontMetricsOptions): MonospaceFontMetrics {
    const { family, sizePx } = options;
    let { sampleChars }  = options;

    if (sampleChars == null) {
      sampleChars = ["X", "W", "_", "g", "\u00C5", "\u00E7", "\u014A", "\u013B","\u0141", "\u0126"];
    }

    const canvas = <HTMLCanvasElement> document.createElement("canvas");

    this._canvasWidthPx = Math.ceil(sizePx * 3);
    this._canvasHeightPx = Math.ceil(sizePx * 3);
    canvas.width = this._canvasWidthPx;
    canvas.height = this._canvasHeightPx;

    const ctx = canvas.getContext("2d", { alpha: false });
    ctx.font = "" + sizePx + "px " + family;
    ctx.textBaseline = "top";

    // Note: most of the properties on a TextMetrics object are behind Blink's experimental flag. 1/5/2019
    const metrics = ctx.measureText(sampleChars[0]);
    // logFontMetrics(sampleChars[0], metrics);
    const charWidthPx = Math.ceil(metrics.width);

    const metricsAscent = metrics.fontBoundingBoxAscent === undefined ? metrics.actualBoundingBoxAscent : metrics.fontBoundingBoxAscent;
    const metricsDescent = metrics.fontBoundingBoxDescent === undefined ? metrics.actualBoundingBoxDescent : metrics.fontBoundingBoxDescent;

    let ascent = Math.floor(-metricsAscent);
    let descent = Math.ceil(metricsDescent);
    for (const sampleChar of sampleChars) {
      const { topY, bottomY } = this._renderAndMeasureText(ctx, sizePx, sampleChar);
      ascent = Math.min(ascent, topY);
      descent = Math.max(descent, bottomY+1);
    }

    let boldItalicWidthPx = charWidthPx;
    ctx.font = "bold italic " + sizePx + "px " + family;
    for (const sampleChar of sampleChars) {
      const { leftX, rightX } = this._renderAndMeasureText(ctx, sizePx, sampleChar);
      boldItalicWidthPx = Math.max(boldItalicWidthPx, rightX - leftX);
    }

    const fillTextYOffset = -ascent;
    const charHeightPx = descent - ascent;

    // this._log.debug(`charWidthPx: ${charWidthPx }, charHeightPx: ${charHeightPx}, fillTextYOffset: ${fillTextYOffset}`);

    // Used for the strike through and underline Y positions.
    const {topY: mTopY, bottomY: mBottomY} = this._renderAndMeasureText(ctx, sizePx, "m");
    // this._log.debug(`m: topY: ${mTopY}, bottomY: ${mBottomY}`);

    const underlineHeight = 1;
    const underlineY = Math.round(mBottomY + 2) + fillTextYOffset;
    const secondUnderlineY = underlineY + 2 * underlineHeight;

    const curlyThickness = 1;
    const curlyHeight = 4;
    const curlyY = underlineY + curlyHeight/2;

    return {
      fontSizePx: sizePx,
      fontFamily: family,

      widthPx: charWidthPx,
      heightPx: charHeightPx,
      boldItalicWidthPx,
  
      fillTextYOffset,
      fillTextXOffset: 0,

      strikethroughY: Math.round((mTopY + mBottomY) /2) + fillTextYOffset,
      strikethroughHeight: 1,<commit_msg>Tweak the positioning of underlines for small fonts<commit_after>/**
 * Copyright 2020 <NAME> <<EMAIL>>
 */

import { MonospaceFontMetrics } from "./MonospaceFontMetrics";
import { Logger, getLogger, log } from "extraterm-logging";

const computeFontMetricsCache = new Map<string, MonospaceFontMetrics>();
const computeDpiFontMetricsCache = new Map<string, { renderFontMetrics: MonospaceFontMetrics, cssFontMetrics: MonospaceFontMetrics }>();


export function computeFontMetrics(fontFamily: string, fontSizePx: number, sampleChars: string[]=null): MonospaceFontMetrics {
  const cacheKey = `${fontFamily};${fontSizePx};${sampleChars == null ? "" : sampleChars.join("")}`;

  let metrics = computeFontMetricsCache.get(cacheKey);
  if (metrics != null) {
    return metrics;
  }

  const fm = new FontMeasurement();
  metrics = fm.computeFontMetrics({ family: fontFamily, sizePx: fontSizePx, sampleChars });

  computeFontMetricsCache.set(cacheKey, metrics);
  return metrics;
}

export function computeDpiFontMetrics(fontFamily: string, fontSizePx: number, devicePixelRatio: number,
    sampleChars: string[]=null): { renderFontMetrics: MonospaceFontMetrics, cssFontMetrics: MonospaceFontMetrics } {

  const cacheKey = `${fontFamily};${fontSizePx};${devicePixelRatio};${sampleChars == null ? "" : sampleChars.join("")}`;
  let metrics = computeDpiFontMetricsCache.get(cacheKey);
  if (metrics != null) {
    return metrics;
  }

  const fm = new FontMeasurement();
  const renderFontSizePx = fontSizePx * devicePixelRatio;
  const renderFontMetrics = fm.computeFontMetrics({ family: fontFamily, sizePx: renderFontSizePx, sampleChars });

  const cssFontMetrics: MonospaceFontMetrics = {
    fontSizePx: renderFontMetrics.fontSizePx / devicePixelRatio,
    fontFamily,

    fillTextYOffset: renderFontMetrics.fillTextYOffset / devicePixelRatio,
    fillTextXOffset: renderFontMetrics.fillTextYOffset / devicePixelRatio,

    widthPx: renderFontMetrics.widthPx / devicePixelRatio,
    heightPx: renderFontMetrics.heightPx / devicePixelRatio,
    boldItalicWidthPx: renderFontMetrics.boldItalicWidthPx / devicePixelRatio,

    strikethroughY: renderFontMetrics.strikethroughY / devicePixelRatio,
    strikethroughHeight: renderFontMetrics.strikethroughHeight / devicePixelRatio,
    underlineY: renderFontMetrics.underlineY / devicePixelRatio,
    secondUnderlineY: renderFontMetrics.secondUnderlineY / devicePixelRatio,
    underlineHeight: renderFontMetrics.underlineHeight / devicePixelRatio,

    overlineY: renderFontMetrics.overlineY / devicePixelRatio,
    overlineHeight: renderFontMetrics.overlineHeight / devicePixelRatio,

    curlyHeight: renderFontMetrics.curlyHeight / devicePixelRatio,
    curlyThickness: renderFontMetrics.curlyThickness / devicePixelRatio,
    curlyY: renderFontMetrics.curlyY / devicePixelRatio,
  };

  metrics = {
    renderFontMetrics,
    cssFontMetrics,
  };

  computeDpiFontMetricsCache.set(cacheKey, metrics);
  return metrics;
}

interface TextMeasurements {
  topY: number;
  bottomY: number;
  leftX: number;
  rightX: number;
}

interface ComputeFontMetricsOptions {
  family: string;
  sizePx: number;
  sampleChars?: string[];
}

class FontMeasurement {
  private _log: Logger = null;
  private _canvasWidthPx = 250;
  private _canvasHeightPx = 250;

  constructor() {
    this._log = getLogger("FontMeasurement", this);
  }

  computeFontMetrics(options: ComputeFontMetricsOptions): MonospaceFontMetrics {
    const { family, sizePx } = options;
    let { sampleChars }  = options;

    if (sampleChars == null) {
      sampleChars = ["X", "W", "_", "g", "\u00C5", "\u00E7", "\u014A", "\u013B","\u0141", "\u0126"];
    }

    const canvas = <HTMLCanvasElement> document.createElement("canvas");

    this._canvasWidthPx = Math.ceil(sizePx * 3);
    this._canvasHeightPx = Math.ceil(sizePx * 3);
    canvas.width = this._canvasWidthPx;
    canvas.height = this._canvasHeightPx;

    const ctx = canvas.getContext("2d", { alpha: false });
    ctx.font = "" + sizePx + "px " + family;
    ctx.textBaseline = "top";

    // Note: most of the properties on a TextMetrics object are behind Blink's experimental flag. 1/5/2019
    const metrics = ctx.measureText(sampleChars[0]);
    // logFontMetrics(sampleChars[0], metrics);
    const charWidthPx = Math.ceil(metrics.width);

    const metricsAscent = metrics.fontBoundingBoxAscent === undefined ? metrics.actualBoundingBoxAscent : metrics.fontBoundingBoxAscent;
    const metricsDescent = metrics.fontBoundingBoxDescent === undefined ? metrics.actualBoundingBoxDescent : metrics.fontBoundingBoxDescent;

    let ascent = Math.floor(-metricsAscent);
    let descent = Math.ceil(metricsDescent);
    for (const sampleChar of sampleChars) {
      const { topY, bottomY } = this._renderAndMeasureText(ctx, sizePx, sampleChar);
      ascent = Math.min(ascent, topY);
      descent = Math.max(descent, bottomY+1);
    }

    let boldItalicWidthPx = charWidthPx;
    ctx.font = "bold italic " + sizePx + "px " + family;
    for (const sampleChar of sampleChars) {
      const { leftX, rightX } = this._renderAndMeasureText(ctx, sizePx, sampleChar);
      boldItalicWidthPx = Math.max(boldItalicWidthPx, rightX - leftX);
    }

    const fillTextYOffset = -ascent;
    const charHeightPx = descent - ascent;

    // this._log.debug(`charWidthPx: ${charWidthPx }, charHeightPx: ${charHeightPx}, fillTextYOffset: ${fillTextYOffset}`);

    // Used for the strike through and underline Y positions.
    const {topY: mTopY, bottomY: mBottomY} = this._renderAndMeasureText(ctx, sizePx, "m");
    // this._log.debug(`m: topY: ${mTopY}, bottomY: ${mBottomY}`);

    const underlineHeight = 1;
    let underlineY = 0;
    let secondUnderlineY = 0;
    const clearanceUnderChar = charHeightPx - Math.round(mBottomY);
    if (clearanceUnderChar < 4) {
      underlineY = Math.round(mBottomY) + 1 + fillTextYOffset;
      secondUnderlineY = Math.min(underlineY + 2 * underlineHeight, charHeightPx-1);
    } else {
      underlineY = Math.round(mBottomY) + 2 + fillTextYOffset;
      secondUnderlineY = Math.min(underlineY + 2 * underlineHeight, charHeightPx-1);
    }

    const curlyThickness = 1;
    const curlyHeight = 4;
    const curlyY = underlineY + curlyHeight/2;

    return {
      fontSizePx: sizePx,
      fontFamily: family,

      widthPx: charWidthPx,
      heightPx: charHeightPx,
      boldItalicWidthPx,

      fillTextYOffset,
      fillTextXOffset: 0,

      strikethroughY: Math.round((mTopY + mBottomY) /2) + fillTextYOffset,
      strikethroughHeight: 1,
{'fraction_non_alphanumeric': 0.10823972979341764, 'fraction_numerical': 0.008561778336344356, 'mean_word_length': 4.431740614334471, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1465624', 'n_tokens_mistral': 4101, 'n_tokens_neox': 3873, 'n_words': 844}