Skip to content

Commit

Permalink
fix: text decoration line (#531)
Browse files Browse the repository at this point in the history
### Description
1. Default to the current color when `textDecorationColor` is unset.
2. Resolve incorrect positioning of decoration line.
3. Address inaccurate line-through positioning.

<img width="1438" alt="image"
src="https://github.com/vercel/satori/assets/22126563/71e401e1-f03d-45e4-a5ad-c202487ad605">

<img width="912" alt="image"
src="https://github.com/vercel/satori/assets/22126563/fe010957-ce7c-401d-bf02-e59e1a03a2a9">

Closes: #530
  • Loading branch information
LuciNyan authored Aug 31, 2023
1 parent 5c37104 commit c47e1a9
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 26 deletions.
5 changes: 3 additions & 2 deletions src/builder/text-decoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function buildDecoration(
textDecorationStyle,
textDecorationLine,
fontSize,
color,
} = style
if (!textDecorationLine || textDecorationLine === 'none') return ''

Expand All @@ -30,7 +31,7 @@ export default function buildDecoration(

const y =
textDecorationLine === 'line-through'
? top + ascender * 0.5
? top + ascender * 0.7
: textDecorationLine === 'underline'
? top + ascender * 1.1
: top
Expand All @@ -47,7 +48,7 @@ export default function buildDecoration(
y1: y,
x2: left + width,
y2: y,
stroke: textDecorationColor,
stroke: textDecorationColor || color,
'stroke-width': height,
'stroke-dasharray': dasharray,
'stroke-linecap': textDecorationStyle === 'dotted' ? 'round' : 'square',
Expand Down
47 changes: 23 additions & 24 deletions src/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,9 +541,16 @@ export default async function* buildTextNodes(
}
}

const baselineOfLine = baselines[line]
const baselineOfWord = engine.baseline(text)
const heightOfWord = engine.height(text)
const baselineDelta = baselineOfLine - baselineOfWord

if (!decorationLines[line]) {
decorationLines[line] = [
leftOffset,
top + topOffset + baselineDelta,
baselineOfWord,
extendedWidth ? containerWidth : lineWidths[line],
]
}
Expand Down Expand Up @@ -599,15 +606,15 @@ export default async function* buildTextNodes(

text = subset + _blockEllipsis
skippedLine = line
decorationLines[line][1] = resolvedWidth
decorationLines[line][2] = resolvedWidth
isLastDisplayedBeforeEllipsis = true
} else if (nextLayout && nextLayout.line !== line) {
if (textAlign === 'center') {
const { subset, resolvedWidth } = calcEllipsis(leftOffset, text)

text = subset + _blockEllipsis
skippedLine = line
decorationLines[line][1] = resolvedWidth
decorationLines[line][2] = resolvedWidth
isLastDisplayedBeforeEllipsis = true
} else {
const nextLineText = texts[i + 1]
Expand All @@ -619,18 +626,13 @@ export default async function* buildTextNodes(

text = text + subset + _blockEllipsis
skippedLine = line
decorationLines[line][1] = resolvedWidth
decorationLines[line][2] = resolvedWidth
isLastDisplayedBeforeEllipsis = true
}
}
}
}

const baselineOfLine = baselines[line]
const baselineOfWord = engine.baseline(text)
const heightOfWord = engine.height(text)
const baselineDelta = baselineOfLine - baselineOfWord

if (image) {
// For images, we remove the baseline offset.
topOffset += 0
Expand Down Expand Up @@ -703,22 +705,19 @@ export default async function* buildTextNodes(

// Get the decoration shape.
if (parentStyle.textDecorationLine) {
// If it's the last word in the current line.
if (line !== nextLayout?.line || skippedLine === line) {
const deco = decorationLines[line]
if (deco && !deco[2]) {
decorationShape += buildDecoration(
{
left: left + deco[0],
top: top + heightOfWord * +line,
width: deco[1],
ascender: engine.baseline(text),
clipPathId,
},
parentStyle
)
deco[2] = 1
}
const deco = decorationLines[line]
if (deco && !deco[4]) {
decorationShape += buildDecoration(
{
left: left + deco[0],
top: deco[1],
width: deco[3],
ascender: deco[2],
clipPathId,
},
parentStyle
)
deco[4] = 1
}
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
155 changes: 155 additions & 0 deletions test/text-decoration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { it, describe, expect } from 'vitest'

import { initFonts, loadDynamicAsset, toImage } from './utils.js'
import satori from '../src/index.js'

describe('Text Decoration', () => {
let fonts
initFonts((f) => (fonts = f))

it('Should work correctly when `text-decoration-line: line-through` and `text-align: right`', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
fontSize: 20,
fontWeight: 600,
}}
>
<div
style={{
maxWidth: '190px',
backgroundColor: '#91a8d0',
textDecorationLine: 'line-through',
color: 'white',
textAlign: 'center',
}}
>
你好! It doesn’t 안녕! exist, it never has. I’m nostalgic for a place
that never existed.
</div>
</div>,
{
width: 200,
height: 200,
fonts,
loadAdditionalAsset: (languageCode: string, segment: string) => {
return loadDynamicAsset(languageCode, segment) as any
},
}
)
expect(toImage(svg, 200)).toMatchImageSnapshot()
})

it('Should work correctly when `text-decoration-line: underline` and `text-align: right`', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
fontSize: 20,
fontWeight: 600,
}}
>
<div
style={{
maxWidth: '190px',
backgroundColor: '#91a8d0',
textDecorationLine: 'underline',
color: 'white',
textAlign: 'right',
}}
>
你好! It doesn’t 안녕! exist, it never has. I’m nostalgic for a place
that never existed.
</div>
</div>,
{
width: 200,
height: 200,
fonts,
loadAdditionalAsset: (languageCode: string, segment: string) => {
return loadDynamicAsset(languageCode, segment) as any
},
}
)
expect(toImage(svg, 200)).toMatchImageSnapshot()
})

it('Should work correctly when `text-decoration-style: dotted`', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
fontSize: 20,
fontWeight: 600,
}}
>
<div
style={{
maxWidth: '190px',
backgroundColor: '#91a8d0',
textDecorationLine: 'underline',
textDecorationStyle: 'dotted',
color: 'white',
}}
>
It doesn’t exist, it never has. I’m nostalgic for a place that never
existed.
</div>
</div>,
{ width: 200, height: 200, fonts }
)
expect(toImage(svg, 200)).toMatchImageSnapshot()
})

it('Should work correctly when `text-decoration-style: dashed`', async () => {
const svg = await satori(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
fontSize: 20,
fontWeight: 600,
}}
>
<div
style={{
maxWidth: '190px',
backgroundColor: '#91a8d0',
textDecorationLine: 'underline',
textDecorationStyle: 'dashed',
color: 'white',
}}
>
It doesn’t exist, it never has. I’m nostalgic for a place that never
existed.
</div>
</div>,
{ width: 200, height: 200, fonts }
)
expect(toImage(svg, 200)).toMatchImageSnapshot()
})
})

1 comment on commit c47e1a9

@vercel
Copy link

@vercel vercel bot commented on c47e1a9 Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.