Open andykais opened 3 years ago
[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/canvas@v1.3.0/src/lib.js:2264:37)
at <anonymous>:2:10
For loading custom fonts, there is canvas.loadFont
.
For setting font you'll need to specify size too.
And deno-canvas cannot access system fonts either btw, so initially it's limited to one font IIRC.
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?
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;
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
}
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
}
if this looks like a real issue I can create a separate issue or change the title on this one
Here are some repros. First a jsfiddle which correctly measures the width of text: https://jsfiddle.net/8dk71toq/2/
and second, deno-canvas incorrectly measuring the same text:
import { createCanvas } from 'https://deno.land/x/canvas@v1.3.0/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())
both examples use this font https://fonts.google.com/specimen/Tangerine?query=tangerine
some further debugging shows that the builtin font (monospace
in the case of my mac) correctly measures the text. 50px monospace
is shown below.
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?
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.
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
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.
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)
Skia related APIs are all exposed in a namespace that is default exported from mod.ts
.
import Skia from "https://deno.land/x/canvas@v1.3.0/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.
@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?
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
.
@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/canvas@v1.3.0/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:
fwiw I created an issue with skia https://bugs.chromium.org/p/skia/issues/detail?id=12586
@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 🤔
whats the current CanvasKit version?
It is 0.32.0
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;
@webdev3301 I believe there is still an issue with measuring custom loaded fonts. See this message above: https://github.com/DjDeveloperr/deno-canvas/issues/17#issuecomment-926166920
deno-canvas supports writing text to a canvas:
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.
the
registerFont
method is very similar to the css interface for loading fonts: