fix(studio): bold text measured at each family's own bold width, so a bold condensed heading no longer overruns its frame

This commit is contained in:
Maurice
2026-10-24 09:44:52 +02:00
parent 9138e722bb
commit f14f1e41b9
4 changed files with 42 additions and 3 deletions
+7 -2
View File
@@ -76,7 +76,12 @@ export function photoDpi(el: BookPhotoElement, info: BookPhotoInfo | undefined):
}
/** Average letter width per em at weight 400, per family: the catalogue's measure (book-fonts.data.ts). */
const letterEm = (font: string) => bookFontFace(font)?.letterEm ?? 0.5;
/** A family's average letter width per em at a weight: its measured bold from 600 on, else a guess a little wider. */
function letterEm(font: string, weight: number): number {
const face = bookFontFace(font);
const regular = face?.letterEm ?? 0.5;
return weight >= 600 ? (face?.boldEm ?? regular * 1.06) : regular;
}
/**
* The headroom on letter width the design engine sizes a text frame with. A
@@ -124,7 +129,7 @@ export function bookLetterMm(el: BookTextElement, headroom = 1): number {
const sizeMm = (orDefault(el.size, 11) * 25.4) / 72;
const tracking = orDefault(el.tracking, 0);
const caps = bookFontFace(el.font)?.caps ? 1 : (CASE_WIDTH[el.transform ?? 'none'] ?? 1);
return sizeMm * (letterEm(el.font) * (el.weight >= 600 ? 1.06 : 1) * caps * headroom + tracking);
return sizeMm * (letterEm(el.font, el.weight) * caps * headroom + tracking);
}
/**
+20
View File
@@ -58,6 +58,11 @@ export interface BookFontFace {
* the face's own advance widths, for the text measure in book-findings.ts.
*/
letterEm: number;
/**
* The same at weight 700, measured the same way, for a family that ships a
* real bold: a condensed bold sets up to a sixth wider than its regular.
*/
boldEm?: number;
/** Set in capitals whatever is typed. */
caps?: true;
}
@@ -81,6 +86,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.507,
boldEm: 0.519,
},
inter: {
name: 'Inter',
@@ -91,6 +97,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.478,
boldEm: 0.491,
},
montserrat: {
name: 'Montserrat',
@@ -101,6 +108,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.507,
boldEm: 0.536,
},
// ── serif ──
serif: {
@@ -112,6 +120,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: MID,
letterEm: 0.469,
boldEm: 0.481,
},
garamond: {
name: 'EB Garamond',
@@ -122,6 +131,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800],
italics: [400, 500, 600, 700, 800],
letterEm: 0.383,
boldEm: 0.424,
},
// ── elegant display serifs ──
playfair: {
@@ -133,6 +143,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800, 900],
italics: [400, 500, 600, 700, 800, 900],
letterEm: 0.454,
boldEm: 0.462,
},
fraunces: {
name: 'Fraunces',
@@ -143,6 +154,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: ALL,
letterEm: 0.467,
boldEm: 0.497,
},
cormorant: {
name: 'Cormorant Garamond',
@@ -153,6 +165,7 @@ export const BOOK_FONT_FACES = {
weights: [300, 400, 500, 600, 700],
italics: [300, 400, 500, 600, 700],
letterEm: 0.393,
boldEm: 0.398,
},
dmserif: {
name: 'DM Serif Display',
@@ -173,6 +186,7 @@ export const BOOK_FONT_FACES = {
weights: [400, 500, 600, 700, 800, 900],
italics: [400, 500, 600, 700, 800, 900],
letterEm: 0.463,
boldEm: 0.488,
},
// ── condensed and poster ──
bebas: {
@@ -205,6 +219,7 @@ export const BOOK_FONT_FACES = {
weights: [200, 300, 400, 500, 600, 700],
italics: [],
letterEm: 0.365,
boldEm: 0.412,
},
league: {
name: 'League Gothic',
@@ -225,6 +240,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.304,
boldEm: 0.355,
},
archivo: {
name: 'Archivo Black',
@@ -246,6 +262,7 @@ export const BOOK_FONT_FACES = {
weights: ALL,
italics: [],
letterEm: 0.492,
boldEm: 0.5,
},
shrikhand: {
name: 'Shrikhand',
@@ -276,6 +293,7 @@ export const BOOK_FONT_FACES = {
weights: [200, 300, 400, 500, 600, 700, 800],
italics: [],
letterEm: 0.471,
boldEm: 0.49,
},
abril: {
name: 'Abril Fatface',
@@ -297,6 +315,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: [],
letterEm: 0.344,
boldEm: 0.349,
},
dancing: {
name: 'Dancing Script',
@@ -307,6 +326,7 @@ export const BOOK_FONT_FACES = {
weights: MID,
italics: [],
letterEm: 0.363,
boldEm: 0.372,
},
pacifico: {
name: 'Pacifico',
+6
View File
@@ -71,6 +71,12 @@ describe('the catalogue', () => {
for (const w of face.italics) expect(face.weights, `${id} italic ${w}`).toContain(w);
expect(face.letterEm, id).toBeGreaterThan(0.2);
expect(face.letterEm, id).toBeLessThan(0.7);
// A measured bold only where the family ships one, and never narrower than its regular.
const bold = (face as { boldEm?: number }).boldEm;
if (bold !== undefined) {
expect(face.weights as readonly number[], id).toContain(700);
expect(bold, id).toBeGreaterThanOrEqual(face.letterEm);
}
expect(face.roles.length, id).toBeGreaterThan(0);
}
});
+9 -1
View File
@@ -1,4 +1,4 @@
import { textHeightNeeded } from './book-findings';
import { bookLetterMm, textHeightNeeded } from './book-findings';
import { blankBook } from './book-pages';
import {
bookCasedText,
@@ -138,4 +138,12 @@ describe('fitting a page', () => {
expect((next.spreads[0]?.elements[0] as BookTextElement).size).toBeLessThan(60);
expect(next.spreads[1]).toBe(doc.spreads[1]);
});
it('BKFIT-011: a bold condensed face is measured at its own width, not the regular widened by a guess', () => {
const oswald = (weight: number) => bookLetterMm(text({ font: 'oswald', weight, size: 20 }));
// Oswald's 700 sets about 13% wider than its 400; a flat 6% would let a bold heading overrun its frame.
expect(oswald(700) / oswald(400)).toBeGreaterThan(1.1);
const vibes = (weight: number) => bookLetterMm(text({ font: 'vibes', weight, size: 20 }));
expect(vibes(700) / vibes(400)).toBeCloseTo(1.06, 6);
});
});