Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/utils/layout/create-page-layout-settings.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);

Expand Down
89 changes: 89 additions & 0 deletions test/utils/layout/create-page-layout-settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
Loading