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

[feature request] loading custom font support #17

Open
andykais opened this issue Sep 20, 2021 · 22 comments
Open

[feature request] loading custom font support #17

andykais opened this issue Sep 20, 2021 · 22 comments

Comments

@andykais
Copy link

andykais commented Sep 20, 2021

deno-canvas supports writing text to a canvas:

import { createCanvas } from 'https://deno.land/x/canvas/mod.ts'

const canvas = createCanvas(500,600)
const ctx = canvas.getContext('2d')
ctx.fillStyle='red'
ctx.fillText(50,50,"Hello World")
await Deno.writeFile("image.png", canvas.toBuffer());

this is currently only limited to fonts your system knows about (it might even be more limited than that). Canvas kit has an api for loading custom fonts https://skia.org/docs/user/modules/quickstart/#text-shaping. Itd be great if deno-canvas supported loading custom fonts. I think pulling in the whole paragraph builder api might be substantial, but all I would personally be interested in is mirroring the browser's canvas apis with the addition of being able to load custom fonts. E.g.

import { createCanvas, registerFont } from 'https://deno.land/x/canvas/mod.ts'

const canvas = createCanvas(500,600)
const ctx = canvas.getContext('2d')
await registerFont({
  font_family: 'Comic Sans',
  // extra fancy would be supporting a url here (e.g. file:///my-fonts/comic-sans.ttf or any web url)
  src: './my-fonts/comic-sans.ttf'
})
ctx.font = 'Comic Sans'
ctx.fillStyle='red'
ctx.fillText(50,50,"Hello World")
await Deno.writeFile("image.png", canvas.toBuffer());

the registerFont method is very similar to the css interface for loading fonts:

@font-face {
    font-family: 'KulminoituvaRegular';
    src: url('http://www.miketaylr.com/f/kulminoituva.ttf');
}
@andykais andykais changed the title [feature request] loading font family support [feature request] loading custom font support Sep 20, 2021
@andykais
Copy link
Author

[edit]
it seems like ctx.font = is not a supported api at all in the current version. Setting font to anything, including fonts available to the system looks like:

> ctx.font = 'Roboto'
Uncaught TypeError: Cannot read property 'family' of null
    at q.set [as font] (https://deno.land/x/[email protected]/src/lib.js:2264:37)
    at <anonymous>:2:10

@DjDeveloperr
Copy link
Owner

DjDeveloperr commented Sep 22, 2021

For loading custom fonts, there is canvas.loadFont.

For setting font you'll need to specify size too.

image

And deno-canvas cannot access system fonts either btw, so initially it's limited to one font IIRC.

@andykais
Copy link
Author

oh if this is already supported thats fantastic. I am looking at the code now https://github.com/DjDeveloperr/deno-canvas/blob/master/src/types.ts#L1065, what should the descriptors field be?

@andykais
Copy link
Author

andykais commented Sep 22, 2021

ah, I figured it out:

canvas.loadFont(fontBuffer, {
  family: 'Comic Sans',
  style: 'normal',
  weight: 'normal',
  variant: 'normal'
})

it probably wouldnt hurt to make the type signatures more specific than Record<string, string>. Perhaps:

type FontDescriptors = {
  /* identifying name of font */
  family: string
  style: 'normal' | 'italic'
  variant: 'normal' | ...
  weight: 'normal' | 'bold' | ...
}
  loadFont(
    bytes: ArrayBuffer | Uint8Array,
    descriptors: FontDescriptors,
  ): void;

@andykais
Copy link
Author

andykais commented Sep 23, 2021

seems like the context.measureText results are very inaccurate.

const text = "Hello There"
const family = './fonts/stick-no-bills/StickNoBills-VariableFont_wght.ttf'
// load a font
    const font = await Deno.readFile(family)
    const font_identifier = new Date().toString()
    canvas.loadFont(font, {
      family: font_identifier
    })
    context.font = `${size}px ${font_identifier}`
// get the font measurements
  const metrics = context.measureText(text)
// draw a rect around it
    context.fillStyle = 'white'
    context.fillRect(0, 0, metrics.width, metrics.fontBoundingBoxAscent + metrics.actualBoundingBoxDescent)
// draw the text
    context.fillStyle =  "black"
    context.fillText(text_chunk, 0, 0)

here they are for one font https://fonts.google.com/specimen/Stick+No+Bills

{
  width: 352,
  actualBoundingBoxAscent: 38,
  actualBoundingBoxDescent: 2,
  actualBoundingBoxLeft: 5,
  actualBoundingBoxRight: 357,
  fontBoundingBoxAscent: 47,
  fontBoundingBoxDescent: 15.600000381469727
}

canvas

here they are for another https://www.fontspace.com/category/comic-sans

{
  width: 198,
  actualBoundingBoxAscent: 33,
  actualBoundingBoxDescent: 1,
  actualBoundingBoxLeft: 0,
  actualBoundingBoxRight: 198,
  fontBoundingBoxAscent: 43.75,
  fontBoundingBoxDescent: 13.600000381469727
}

canvas

@andykais
Copy link
Author

if this looks like a real issue I can create a separate issue or change the title on this one

@andykais
Copy link
Author

Here are some repros. First a jsfiddle which correctly measures the width of text:
https://jsfiddle.net/8dk71toq/2/

Screen Shot 2021-09-24 at 1 08 07 PM

and second, deno-canvas incorrectly measuring the same text:

import { createCanvas } from 'https://deno.land/x/[email protected]/mod.ts'

const canvas = createCanvas(500, 200)
const context = canvas.getContext('2d')

function draw_text(text: string, x: number, y: number) {
  const metrics = context.measureText(text)
  console.log(metrics)
  context.fillStyle = 'red'
  context.fillRect(
    x,
    y,
    metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight,
    metrics.fontBoundingBoxAscent,
  )

  context.fillStyle = 'black'
  context.fillText(
    text,
    x + metrics.actualBoundingBoxLeft,
    y + metrics.fontBoundingBoxAscent,
  )
}

context.fillStyle = 'white'
context.fillRect(0, 0, canvas.width, canvas.height)
const font_buffer = await Deno.readFile('./fonts/tangerine/Tangerine-Regular.ttf')
canvas.loadFont(font_buffer, {family: 'Tangerine'})
context.font = '50px Tangerine'
draw_text("Hello World", 50, 50)
await Deno.writeFile('canvas.png', canvas.toBuffer())

canvas

both examples use this font https://fonts.google.com/specimen/Tangerine?query=tangerine

@andykais
Copy link
Author

some further debugging shows that the builtin font (monospace in the case of my mac) correctly measures the text. 50px monospace is shown below.
canvas

loading a different font and specifying it via context.font does produce different measurements, so its clear that canvaskit is reading the new font. It just doesnt quite interpret the sizes correctly. Is this an issue I should move upstream to canvaskit?

@DjDeveloperr
Copy link
Owner

All values in TextMetrics except width were added with a hacky workaround, so they might not be right. And no, actual canvaskit does not even implement measureText as they just say to use Paragraph API instead. So this is a issue in deno-canvas only.

@andykais
Copy link
Author

Got it. Well it is unfortunate that measuring text can't be done with deno canvas. I wanted to build out text captions with borders. I suppose there's an extremely hacky workaround for me where I dump the image data and find the max & min x & y

@DjDeveloperr
Copy link
Owner

Right.. text rendering as a whole is not-so-good with canvaskit-wasm. Btw, deno-canvas does expose Skia related APIs, such as Paragraph API if that's any helpful for you. I plan on porting Node.js skia-canvas using Deno FFI, if that works out we'd have a more performant and compatible canvas API. Though can't say for sure it'll be a thing, or anytime soon as FFI is limited a lot right now.

@andykais
Copy link
Author

It would definitely be cool to see an ffi bridge to skia. For what I need though, if the paragraph api is exposed that would probably get me everything I need. Would you mind showing an example of how to use it in deno canvas (I have seen canvaskits docs but I'm not sure it's the same api here)

@DjDeveloperr
Copy link
Owner

DjDeveloperr commented Sep 27, 2021

Skia related APIs are all exposed in a namespace that is default exported from mod.ts.

import Skia from "https://deno.land/x/[email protected]/mod.ts";

// Use Skia.Paragraph, Skia.ParagraphBuilder, etc.
// API should be same as canvaskit.

Under the hood, canvaskit polyills HTML Canvas using Skia WASM API. If you don't mind, I'd appreciate a PR to polyfill measureText properly using Paragraph API.

@andykais
Copy link
Author

andykais commented Oct 1, 2021

@DjDeveloperr could you give me a primer on contributing to this library? Would it be this file https://github.com/DjDeveloperr/deno-canvas/blob/master/src/lib.js#L2750? Its hard to tell if this is a source file because some of this code looks minified. Could you show where another skia specific method is used as an example?

@DjDeveloperr
Copy link
Owner

Yeah, in src/lib.js. Right.. it was minified at first. a variable contains everything exported as default in mod.ts, so all Skia APIs. You can use ctrl+f to find implementation of measureText.

@andykais
Copy link
Author

andykais commented Oct 28, 2021

@DjDeveloperr its been a minute since I worked on this. Heres a first attempt at measuring text. I can properly measure ascent and descent of text from the baseline. I cant seem to measure left/right properly though. Especially with this font which is extra italicized (this is the "Regular") font. I know if there is a part of the paragraph api I am just missing or what

import CanvasKit, { createCanvas } from "https://deno.land/x/[email protected]/mod.ts";

function measureText(text: string, fontInfo: { fontData: Uint8Array, fontSize: number }) {
  // I assume I can find fontInfo somewhere else inside the context class
  const { fontData, fontSize } = fontInfo
  const fontMgr = CanvasKit.FontMgr.FromData(fontData);
  if (fontMgr === null)  throw new Error('idk why but fontMgr is null')
  const paraStyle = new CanvasKit.ParagraphStyle({
    textStyle: {
      color: CanvasKit.BLACK,
      fontFamilies: [fontMgr.getFamilyName(0)],
      fontSize,
    },
  });
  const builder = CanvasKit.ParagraphBuilder.Make(paraStyle, fontMgr);
  builder.addText(text);
  const paragraph = builder.build();
  paragraph.layout(Infinity);
  const left = Math.max(...paragraph.getLineMetrics().map(l => l.left))
  const right = paragraph.getLongestLine() + left
  const ascent = Math.max(...paragraph.getLineMetrics().map(l => l.ascent))
  const descent = Math.max(...paragraph.getLineMetrics().map(l => l.descent))
  const height = ascent + descent
  const width = right
  const metrics = { ascent, descent, left, right, width, height }
  paragraph.delete()
  fontMgr.delete()
  return metrics
}

function draw_text(text: string, x: number, y: number, fontInfo: { fontData: Uint8Array, fontSize: number }) {
  const metrics = measureText(text, fontInfo)
  console.log(metrics)
  context.fillStyle = 'red'
  context.fillRect(
    x - metrics.left,
    y - metrics.ascent,
    metrics.width,
    metrics.height,
  )

  context.fillStyle = 'black'
  context.fillText(
    text,
    x,
    y,
  )
}


const fontSize =  50
const canvas = createCanvas(500, 200)
const context = canvas.getContext('2d')
context.fillStyle = 'white'
context.fillRect(0, 0, canvas.width, canvas.height)
canvas.loadFont(fontData, {family: 'Tangerine'})
context.font = `${fontSize}px Tangerine`
// context.textBaseline = 'top'
draw_text("Hello go World", 50, 50, { fontSize, fontData })
await Deno.writeFile('canvas.png', canvas.toBuffer())

the output:
canvas

@andykais
Copy link
Author

fwiw I created an issue with skia https://bugs.chromium.org/p/skia/issues/detail?id=12586

@DjDeveloperr
Copy link
Owner

DjDeveloperr commented Oct 30, 2021

@andykais your first attempt at the implementation looks great. I think it would be good enough for first pass (better than current inaccurate implementation)

And yes, we do have access to Font in context (it's a minified property, which we can access through context.we), and even FontMgr (in canvas.Cf).

So I think you can PR this function (but slightly modified to accept FontMgr and Font instead of fontSize and fontData), and I can add it in lib.js as a follow up 🤔

@andykais
Copy link
Author

whats the current CanvasKit version?

@DjDeveloperr
Copy link
Owner

It is 0.32.0

@suchislife801
Copy link

suchislife801 commented Jul 22, 2022

I think I've figure out how to measure text correctly.

// Make sure to set the font size first!
 context2d.font = `16px sans-serif`;

 // The text to display
 const text: string = `Hello World`;

 // Measure the width of a single character
 const charWidth: number = Math.floor(context2d.measureText("X").width);

 // Get the length of the text string
 const textLength: number = text.length;

 // textWidth at selected fontSize
 const textWidth: number = charWidth * textLength;

@andykais
Copy link
Author

@webdev3301 I believe there is still an issue with measuring custom loaded fonts. See this message above: #17 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants