diff --git a/src/utils/layout/create-page-layout-settings.ts b/src/utils/layout/create-page-layout-settings.ts index f33ef82..39291b1 100644 --- a/src/utils/layout/create-page-layout-settings.ts +++ b/src/utils/layout/create-page-layout-settings.ts @@ -1,4 +1,4 @@ -import calculatePageLayout from '@app/utils/layout/calculate-page-layout'; +import calculatePageLayout, {getMaxHeight} from '@app/utils/layout/calculate-page-layout'; import {hasPageNumbers, hasSectionPageNumbers} from '@app/utils/layout/has-page-numbers'; import type {SectionSettings, SectionSetting} from '@app/evaluators/section-settings'; @@ -34,14 +34,47 @@ interface CreatePageLayoutSettingsOpts { bodyHeightMinimumFactor: number; } +/** + * When the combined header + footer height exceeds the available page space + * (leaving less than bodyHeightMinimumFactor of the page for the body), + * proportionally reduce all section setting heights so they fit. + * + * Returns the original settings unchanged if no capping is needed, + * or cloned settings with reduced heights if capping is required. + */ +function capOversizedSections(sectionSettings: SectionSettings, opts: CreatePageLayoutSettingsOpts): SectionSettings { + const {pageHeight, bodyHeightMinimumFactor} = opts; + if (!pageHeight) return sectionSettings; + + const headerHeight = getMaxHeight(sectionSettings.headers); + const footerHeight = getMaxHeight(sectionSettings.footers); + const totalSectionsHeight = headerHeight + footerHeight; + if (!totalSectionsHeight) return sectionSettings; + + // +2 accounts for the overlap pixel added in calculatePageLayout + const maxSectionsHeight = Math.floor(pageHeight * (1 - bodyHeightMinimumFactor)) + 2; + if (totalSectionsHeight <= maxSectionsHeight) return sectionSettings; + + const scaleFactor = maxSectionsHeight / totalSectionsHeight; + const capHeight = (s: SectionSetting): SectionSetting => ({...s, height: Math.floor(s.height * scaleFactor)}); + + return { + headers: sectionSettings.headers.map(capHeight), + footers: sectionSettings.footers.map(capHeight), + backgrounds: sectionSettings.backgrounds, + }; +} + export function createPageLayoutSettings( sectionSettings: SectionSettings | undefined, opts: CreatePageLayoutSettingsOpts ): PageLayout { - const pageLayout = calculatePageLayout(sectionSettings, opts); + const cappedSettings = sectionSettings ? capOversizedSections(sectionSettings, opts) : undefined; + + const pageLayout = calculatePageLayout(cappedSettings, opts); const transparentBg = !!sectionSettings?.backgrounds.length; - const {headers = [], footers = [], backgrounds = []} = sectionSettings ?? {}; + const {headers = [], footers = [], backgrounds = []} = cappedSettings ?? {}; const hasAnySection = !!(headers.length || footers.length || backgrounds.length); diff --git a/test/utils/layout/create-page-layout-settings.test.ts b/test/utils/layout/create-page-layout-settings.test.ts index 9bc6259..1c3a727 100644 --- a/test/utils/layout/create-page-layout-settings.test.ts +++ b/test/utils/layout/create-page-layout-settings.test.ts @@ -179,4 +179,93 @@ describe('createPageLayoutSettings', () => { background: undefined, }); }); + + describe('oversized header/footer capping', () => { + test('caps header and footer proportionally when they exceed available page space', () => { + // Reproduces the real-world bug: A4 page (842px), header 274px, footer 701px + // Without capping: body = 842 - 274 - 701 + 2 = -131px → would throw + const settings: SectionSettings = { + headers: [makeSectionSetting({height: 274})], + footers: [makeSectionSetting({height: 701})], + backgrounds: [], + }; + + const result = createPageLayoutSettings(settings, makeLayoutOpts({height: 842, width: 595, factor: 0.3})); + + // Should not throw — sections are capped to fit + expect(result.body.height).toBeGreaterThan(0); + expect(result.body.height).toBeGreaterThanOrEqual(Math.floor(842 * 0.3)); + + // Capped settings should have reduced heights + const cappedHeaderHeight = result.header!.settings[0].height; + const cappedFooterHeight = result.footer!.settings[0].height; + expect(cappedHeaderHeight).toBeLessThan(274); + expect(cappedFooterHeight).toBeLessThan(701); + + // Proportions should be preserved + const originalRatio = 274 / 701; + const cappedRatio = cappedHeaderHeight / cappedFooterHeight; + expect(cappedRatio).toBeCloseTo(originalRatio, 1); + }); + + test('does not cap sections when they fit within the page', () => { + const settings: SectionSettings = { + headers: [makeSectionSetting({height: 200})], + footers: [makeSectionSetting({height: 150})], + backgrounds: [], + }; + + const result = createPageLayoutSettings(settings, makeLayoutOpts()); + + expect(result.header!.settings[0].height).toBe(200); + expect(result.footer!.settings[0].height).toBe(150); + }); + + test('caps all variants when multiple physical-page variants exist', () => { + const settings: SectionSettings = { + headers: [ + makeSectionSetting({height: 500, physicalPageType: 'first'}), + makeSectionSetting({height: 400, physicalPageType: 'default'}), + ], + footers: [makeSectionSetting({height: 500})], + backgrounds: [], + }; + + // max header = 500, footer = 500, total = 1000 > 1000 * (1 - 1/3) + 2 = 668 + const result = createPageLayoutSettings(settings, makeLayoutOpts()); + + expect(result.header!.settings[0].height).toBeLessThan(500); + expect(result.header!.settings[1].height).toBeLessThan(400); + expect(result.footer!.settings[0].height).toBeLessThan(500); + expect(result.body.height).toBeGreaterThan(0); + }); + + test('preserves page number flags on capped settings', () => { + const settings: SectionSettings = { + headers: [makeSectionSetting({height: 600, hasCurrentPageNumber: true})], + footers: [makeSectionSetting({height: 600, hasTotalPagesNumber: true})], + backgrounds: [], + }; + + const result = createPageLayoutSettings(settings, makeLayoutOpts()); + + expect(result.hasPageNumbers).toBe(true); + expect(result.header!.hasPageNumbers).toBe(true); + expect(result.footer!.hasPageNumbers).toBe(true); + expect(result.header!.settings[0].hasCurrentPageNumber).toBe(true); + expect(result.footer!.settings[0].hasTotalPagesNumber).toBe(true); + }); + + test('does not modify background heights when capping', () => { + const settings: SectionSettings = { + headers: [makeSectionSetting({height: 600})], + footers: [makeSectionSetting({height: 600})], + backgrounds: [makeSectionSetting({height: 1000})], + }; + + const result = createPageLayoutSettings(settings, makeLayoutOpts()); + + expect(result.background!.settings[0].height).toBe(1000); + }); + }); });