diff --git a/shared/src/book/book-findings.ts b/shared/src/book/book-findings.ts index 433ea8a77..543d5c005 100644 --- a/shared/src/book/book-findings.ts +++ b/shared/src/book/book-findings.ts @@ -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); } /** diff --git a/shared/src/book/book-fonts.data.ts b/shared/src/book/book-fonts.data.ts index fb85f2398..1f7abdd6c 100644 --- a/shared/src/book/book-fonts.data.ts +++ b/shared/src/book/book-fonts.data.ts @@ -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', diff --git a/shared/src/book/book-fonts.spec.ts b/shared/src/book/book-fonts.spec.ts index 63d786f09..5e807e9e1 100644 --- a/shared/src/book/book-fonts.spec.ts +++ b/shared/src/book/book-fonts.spec.ts @@ -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); } }); diff --git a/shared/src/book/book-text-fit.spec.ts b/shared/src/book/book-text-fit.spec.ts index f51486af2..ed3251cee 100644 --- a/shared/src/book/book-text-fit.spec.ts +++ b/shared/src/book/book-text-fit.spec.ts @@ -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); + }); });