Skip to content

Commit 2403fb7

Browse files
authored
Scale certificate name to fit within available space (#2687)
* Scale name on certificate to fit within available space * Clean up * Move to utils * Named print scaling constant
1 parent 4d89b89 commit 2403fb7

File tree

3 files changed

+137
-9
lines changed

3 files changed

+137
-9
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { pxToPt, getNameStyles } from "./utils"
2+
3+
jest.mock("@react-pdf/renderer", () => ({
4+
Document: "Document",
5+
Page: "Page",
6+
View: "View",
7+
Text: "Text",
8+
Svg: "Svg",
9+
G: "G",
10+
Path: "Path",
11+
Font: {
12+
register: jest.fn(),
13+
registerHyphenationCallback: jest.fn(),
14+
},
15+
Image: "Image",
16+
pdf: jest.fn(),
17+
}))
18+
19+
describe("Certificate PDF", () => {
20+
describe("pxToPt", () => {
21+
it("converts pixels to points correctly", () => {
22+
expect(pxToPt(96)).toBeCloseTo(57.6) // 96 * (72/96) * 0.8
23+
expect(pxToPt(52)).toBeCloseTo(31.2) // 52 * 0.75 * 0.8
24+
expect(pxToPt(206)).toBeCloseTo(123.6)
25+
expect(pxToPt(950)).toBeCloseTo(570)
26+
})
27+
})
28+
29+
describe("Name scaling to ensure page fit", () => {
30+
const baseFontSize = pxToPt(52)
31+
const baselineTop = pxToPt(206)
32+
33+
it("keeps short names <= 23 chars at full size", () => {
34+
const shortName = "Wolfgang Amadeus Mozart"
35+
const styles = getNameStyles(shortName)
36+
37+
expect(styles.fontSize).toBeCloseTo(baseFontSize)
38+
expect(styles.top).toBeCloseTo(baselineTop)
39+
})
40+
41+
it("scales medium-length names <= 33 chars proportionally", () => {
42+
const mediumName = "Sir Bartholomew Fizzlewhisk III"
43+
const styles = getNameStyles(mediumName)
44+
45+
expect(styles.fontSize).toBeLessThan(baseFontSize)
46+
expect(styles.fontSize).toBeGreaterThan(baseFontSize * 0.35)
47+
48+
expect(styles.top).toBeGreaterThan(baselineTop)
49+
})
50+
51+
it("scales longer names <= 47 chars more aggressively", () => {
52+
const longName = "Dr. Maximilian Thunderbolt von Schnitzelhausen"
53+
const styles = getNameStyles(longName)
54+
55+
expect(styles.fontSize).toBeLessThan(baseFontSize)
56+
expect(styles.top).toBeGreaterThan(baselineTop)
57+
58+
const mediumName = "Sir Bartholomew Fizzlewhisk III"
59+
const mediumStyles = getNameStyles(mediumName)
60+
expect(styles.fontSize).toBeLessThan(mediumStyles.fontSize)
61+
})
62+
63+
it("applies minimum scale factor of 35% for very long names <= 112 chars", () => {
64+
const veryLongName =
65+
"His Excellency Count Maximilian Cornelius Archibald Pumpernickel-Wigglesworth-Thunderbolt-Whiskerdoodle III"
66+
const styles = getNameStyles(veryLongName)
67+
68+
const minFontSize = baseFontSize * 0.35
69+
expect(styles.fontSize).toBeCloseTo(minFontSize)
70+
71+
const maxOffset = baseFontSize - minFontSize
72+
expect(styles.top).toBeCloseTo(baselineTop + maxOffset)
73+
})
74+
75+
it("maintains baseline alignment when scaling names", () => {
76+
const names = [
77+
"Wolfgang Amadeus Mozart",
78+
"Sir Bartholomew Fizzlewhisk III",
79+
"Princess Anastasia Beauregard-Winterstone",
80+
"Dr. Maximilian Thunderbolt von Schnitzelhausen",
81+
"Captain Cornelius Pumpernickel-Whiskerdoodle Jones",
82+
"Lady Penelope Wigglesworth-Featherstone de la Fontaine",
83+
"Professor Archibald Bartholomew Higginbotham-Waffletop IV",
84+
]
85+
86+
names.forEach((name) => {
87+
const styles = getNameStyles(name)
88+
89+
// The baseline position should be consistent: baseline = top + fontSize
90+
const baseline = styles.top + styles.fontSize
91+
expect(baseline).toBeCloseTo(baselineTop + baseFontSize, 1)
92+
})
93+
})
94+
})
95+
})

frontends/main/src/app/certificate/[certificateType]/[uuid]/pdf/route.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,7 @@ import {
2424
pdf,
2525
} from "@react-pdf/renderer"
2626
import { redirect } from "next/navigation"
27-
28-
/* Enables use of the CertificatePage pixel units styles
29-
- Browsers print at 96 dpi, PDFs default to 72 dpi
30-
- Scaling factor of 0.8 emulates browser print scaling and better reflect the screen design
31-
*/
32-
const pxToPt = (px: number): number => {
33-
return px * (72 / 96) * 0.8
34-
}
27+
import { pxToPt, getNameStyles } from "./utils"
3528

3629
// https://use.typekit.net/lbk1xay.css
3730
Font.register({
@@ -309,8 +302,9 @@ const CertificateDoc = ({
309302
style={{
310303
color: colors.red,
311304
...typography.h1,
305+
fontSize: getNameStyles(userName).fontSize,
312306
position: "absolute",
313-
top: pxToPt(206),
307+
top: getNameStyles(userName).top,
314308
left: pxToPt(46),
315309
}}
316310
>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Enables use of the CertificatePage pixel units styles
2+
- Browsers print at 96 dpi, PDFs default to 72 dpi
3+
- Scaling factor of 0.8 emulates browser print scaling and better reflect the screen design
4+
*/
5+
const PRINT_SCALING_FACTOR = 0.8
6+
7+
export const pxToPt = (px: number): number => {
8+
return px * (72 / 96) * PRINT_SCALING_FACTOR
9+
}
10+
11+
/**
12+
* Calculate font size and top position for user name based on estimated text width
13+
* to ensure long names fit on the certificate while maintaining baseline alignment
14+
*/
15+
export const getNameStyles = (
16+
name: string,
17+
): { fontSize: number; top: number } => {
18+
const baselineTop = pxToPt(206) // Original top position for full-size name
19+
const baseFontSize = pxToPt(52) // h1 font size
20+
const maxWidth = pxToPt(950) // Maximum available width
21+
22+
// For Neue Haas Grotesk at 52px, approximate average char width is ~60% of font size
23+
const avgCharWidth = baseFontSize * 0.6
24+
25+
const estimatedWidth = name.length * avgCharWidth
26+
27+
let scaleFactor = 1.0
28+
if (estimatedWidth > maxWidth) {
29+
scaleFactor = Math.max(0.35, maxWidth / estimatedWidth)
30+
}
31+
32+
const fontSize = baseFontSize * scaleFactor
33+
34+
// Keep the baseline in the same position
35+
const fontSizeDiff = baseFontSize - fontSize
36+
const top = baselineTop + fontSizeDiff
37+
38+
return { fontSize, top }
39+
}

0 commit comments

Comments
 (0)