Skip to content

Commit

Permalink
Correctly position font baseline and line-height (#599)
Browse files Browse the repository at this point in the history
This patch changes logic how baseline and line-height are calculated, to
match what browsers do and
tools like Figma.

This is especially noticeable on font IBM Plex Sans due to how its
metrics are set up. It was less noticeable on Inter, because previous
calculations somehow arrived at almost correct numbers.

Implementation notes:

- `useOS2Table` is removed, because it’s not what browsers/Figma seem to
be using
- yMax, yMin are not used in text positioning at all
- lineHeight is calculated before as a fraction of fontSize, so height
just recalculates it back

Before:

![Screenshot 2024-03-06 at 19 12
12](https://github.com/vercel/satori/assets/285292/dbe5cacf-4839-4d3b-9c9d-cabd8eb189eb)

After:

![Screenshot 2024-03-06 at 19 12
35](https://github.com/vercel/satori/assets/285292/467012bb-16cc-45e5-bc7e-34cc9e59f2fb)

Background blue/orange text is a static PNG rendered with Figma, black
text is rendered with Satori.

Should solve #577

References:
https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align

![Screenshot 2024-03-06 at 19 22
47](https://github.com/vercel/satori/assets/285292/3215bc2f-8bee-4785-a30e-2868255bc63b)

And https://www.figma.com/blog/line-height-changes/:

![Screenshot 2024-03-06 at 19 27
16](https://github.com/vercel/satori/assets/285292/92aa7800-74cd-4e08-a167-e3e34ac02abf)

Background image (if needed):


https://github.com/vercel/satori/assets/285292/3c8d6a75-cdca-4774-b285-7bd64bae51ee
  • Loading branch information
tonsky authored Jun 2, 2024
1 parent 9bc47fd commit d1dfcce
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 24 deletions.
43 changes: 22 additions & 21 deletions src/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export default class FontLoader {

public getEngine(
fontSize = 16,
lineHeight = 1.2,
lineHeight: number | string = 'normal',
{
fontFamily = 'sans-serif',
fontWeight = 400,
Expand Down Expand Up @@ -314,13 +314,28 @@ export default class FontLoader {
resolvedFont.ascender
return (_ascender / resolvedFont.unitsPerEm) * fontSize
}

const descender = (resolvedFont: opentype.Font, useOS2Table = false) => {
const _descender =
(useOS2Table ? resolvedFont.tables?.os2?.sTypoDescender : 0) ||
resolvedFont.descender
return (_descender / resolvedFont.unitsPerEm) * fontSize
}

const height = (resolvedFont: opentype.Font, useOS2Table = false) => {
if ('string' === typeof lineHeight && 'normal' === lineHeight) {
const _lineGap =
(useOS2Table ? resolvedFont.tables?.os2?.sTypoLineGap : 0) || 0
return (
ascender(resolvedFont, useOS2Table) -
descender(resolvedFont, useOS2Table) +
(_lineGap / resolvedFont.unitsPerEm) * fontSize
)
} else if ('number' === typeof lineHeight) {
return fontSize * lineHeight
}
}

const resolve = (s: string) => {
return resolveFont(s, false)
}
Expand All @@ -340,31 +355,17 @@ export default class FontLoader {
s?: string,
resolvedFont = typeof s === 'undefined' ? fonts[0] : resolveFont(s)
) => {
// https://www.w3.org/TR/css-inline-3/#css-metrics
// https://www.w3.org/TR/CSS2/visudet.html#leading
// Note. It is recommended that implementations that use OpenType or
// TrueType fonts use the metrics "sTypoAscender" and "sTypoDescender"
// from the font's OS/2 table for A and D (after scaling to the current
// element's font size). In the absence of these metrics, the "Ascent"
// and "Descent" metrics from the HHEA table should be used.
const A = ascender(resolvedFont, true)
const D = descender(resolvedFont, true)
const glyphHeight = engine.height(s, resolvedFont)
const { yMax, yMin } = resolvedFont.tables.head

const sGlyphHeight = A - D
const baselineOffset = (yMax / (yMax - yMin) - 1) * sGlyphHeight

return glyphHeight * ((1.2 / lineHeight + 1) / 2) + baselineOffset
const asc = ascender(resolvedFont)
const desc = descender(resolvedFont)
const contentHeight = asc - desc

return asc + (height(resolvedFont) - contentHeight) / 2
},
height: (
s?: string,
resolvedFont = typeof s === 'undefined' ? fonts[0] : resolveFont(s)
) => {
return (
(ascender(resolvedFont) - descender(resolvedFont)) *
(lineHeight / 1.2)
)
return height(resolvedFont)
},
measure: (
s: string,
Expand Down
4 changes: 2 additions & 2 deletions src/handler/expand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ type MainStyle = {
whiteSpace: string
wordBreak: string
textAlign: string
lineHeight: number
lineHeight: number | string
letterSpacing: number

fontFamily: string | string[]
Expand Down Expand Up @@ -352,7 +352,7 @@ export default function expand(

// Line height needs to be relative.
if (prop === 'lineHeight') {
if (typeof value === 'string') {
if (typeof value === 'string' && value !== 'normal') {
value = serializedStyle[prop] =
lengthToNumber(
value,
Expand Down
2 changes: 1 addition & 1 deletion src/satori.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default async function satori(
fontWeight: 'normal',
fontFamily: 'serif',
fontStyle: 'normal',
lineHeight: 1.2,
lineHeight: 'normal',
color: 'black',
opacity: 1,
whiteSpace: 'normal',
Expand Down

0 comments on commit d1dfcce

Please sign in to comment.