Skip to content

Commit

Permalink
feat: add fontFamily option
Browse files Browse the repository at this point in the history
  • Loading branch information
roziscoding committed Jun 13, 2022
1 parent 1759ceb commit 23100c1
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 22 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ npm i shiki-renderer-canvas

## Usage

### Custom Fonts

You can use any font you want by passing the `fontFamily` config.

Keep in mind that the font must be accesible to the canvas instance.

For the browser, the font should be locally installed, and for Node.js, the font should be
[registered](https://github.com/Automattic/node-canvas/#registerfont) in `node-canvas` **before calling `createCanvas`**.

An example of how to register a font in Node.js is available at the [generate-sample.ts](./generate-sample.ts) file.

> ⚠️ Important ⚠️: Although `node-canvas` docs say that `registerFont` is only necessary for fonts not installed in
> the system, I found it to be quite hard to get those to work. The best approach for custom fonts in Node.js is to
> always register them with `node-canvas`.
### Node.js

```typescript
Expand Down
35 changes: 35 additions & 0 deletions generate-sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import fs from 'fs/promises'
import path from 'path'
import { getHighlighter } from 'shiki'
import { createCanvas, registerFont } from 'canvas'
import { getCanvasRenderer } from './src'

const render = async (fontFamily?: string) => {
const code = await fs.readFile(path.join(__dirname, 'src/index.ts'), 'utf8')

const highlighter = await getHighlighter({
langs: ['typescript'],
theme: 'dracula'
})

const canvas = createCanvas(1, 1)
const renderer = getCanvasRenderer(canvas, {
font: { family: fontFamily }
})

const tokens = highlighter.codeToThemedTokens(code, 'typescript')

const image = await renderer.renderToCanvas(tokens)
await fs.writeFile(path.join(__dirname, 'sample.png'), image.toBuffer())
}

if (process.argv.length > 2) {
const fontPath = path.resolve(process.argv[2])
const fontName = path.basename(fontPath, path.extname(fontPath))

console.log(`Registering font at ${fontPath} as ${fontName}`)
registerFont(fontPath, { family: fontName })
render(fontName)
} else {
render()
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "dist/index.js",
"scripts": {
"test": "mocha -b -r ts-node/register test/**/*.ts",
"sample": "ts-node generate-sample.ts",
"build": "tsc",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"build:clean": "npm run clean && npm run build",
Expand Down Expand Up @@ -80,4 +81,4 @@
"dependencies": {
"lodash.merge": "^4.6.2"
}
}
}
32 changes: 15 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,9 @@ import { DeepPartial } from './DeepPartial'
import { getLineHeight, getLongestLineLength, getTextHeight } from './math'
import { drawBoundingBox } from './pencil'

type Options = {
fontSize: number
padding: {
vertical: number
horizontal: number
}
backgroundColor: string
drawPaddingLines: boolean
}

const DEFAULT_OPTIONS: Options = {
const DEFAULT_OPTIONS = {
fontSize: 20,
fontFamily: 'monospace',
padding: {
vertical: 20,
horizontal: 20
Expand All @@ -25,23 +16,30 @@ const DEFAULT_OPTIONS: Options = {
drawPaddingLines: false
}

export type CanvasRendererOptions = typeof DEFAULT_OPTIONS

export function getCanvasRenderer<TCanvas extends HTMLCanvasElement | Canvas>(
canvas: TCanvas,
options: DeepPartial<Options> = {}
options: DeepPartial<CanvasRendererOptions> = {}
) {
const config = merge(DEFAULT_OPTIONS, options)
const { fontSize, padding, backgroundColor } = config
const { fontSize, fontFamily, padding, backgroundColor } = config

const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
if (!ctx) throw new Error('no canvas context')

return {
renderToCanvas(tokens: shiki.IThemedToken[][]) {
const longestLineLength = getLongestLineLength(ctx, tokens, fontSize)
const longestLineLength = getLongestLineLength(
ctx,
tokens,
fontSize,
fontFamily
)

canvas.width = longestLineLength + padding.horizontal * 2
canvas.height =
getTextHeight(ctx, fontSize, tokens) + padding.vertical * 2
getTextHeight(ctx, fontSize, fontFamily, tokens) + padding.vertical * 2

ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
Expand All @@ -54,15 +52,15 @@ export function getCanvasRenderer<TCanvas extends HTMLCanvasElement | Canvas>(
let x = padding.horizontal

for (const token of line) {
ctx.font = `${fontSize}px monospace`
ctx.font = `${fontSize}px ${fontFamily}`
ctx.textBaseline = 'top'
ctx.fillStyle = token.color || '#fff'
ctx.fillText(token.content, x, y)

x += ctx.measureText(token.content).width
}

y += getLineHeight(ctx, fontSize, line)
y += getLineHeight(ctx, fontSize, fontFamily, line)
}

return canvas
Expand Down
11 changes: 7 additions & 4 deletions src/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ export function getAbsoluteWidth(metrics: TextMetrics) {
export function getLongestLineLength(
ctx: CanvasRenderingContext2D,
lines: shiki.IThemedToken[][],
fontSize: number
fontSize: number,
fontFamily: string
) {
const previousFont = `${ctx.font}`
const previousTextBaseline = `${ctx.textBaseline}` as CanvasTextBaseline
ctx.font = `${fontSize}px monospace`
ctx.font = `${fontSize}px ${fontFamily}`
ctx.textBaseline = 'top'

const textLineLengths = lines
Expand All @@ -30,13 +31,14 @@ export function getLongestLineLength(
export function getLineHeight(
ctx: CanvasRenderingContext2D,
fontSize: number,
fontFamily: string,
text: string | shiki.IThemedToken[] = 'M'
) {
const previousFont = `${ctx.font}`
const previousTextBaseline = `${ctx.textBaseline}` as CanvasTextBaseline
const line = Array.isArray(text) ? text.map((t) => t.content).join('') : text

ctx.font = `${fontSize}px monospace`
ctx.font = `${fontSize}px ${fontFamily}`
ctx.textBaseline = 'top'

const measurements = ctx.measureText(line)
Expand All @@ -52,10 +54,11 @@ export function getLineHeight(
export function getTextHeight(
ctx: CanvasRenderingContext2D,
fontSize: number,
fontFamily: string,
lines: shiki.IThemedToken[][]
) {
return lines.reduce((result, line) => {
const lineContent = line.map((token) => token.content).join('')
return result + getLineHeight(ctx, fontSize, lineContent)
return result + getLineHeight(ctx, fontSize, fontFamily, lineContent)
}, 0)
}

0 comments on commit 23100c1

Please sign in to comment.