Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ export function computeConstraintsHash(constraints: HeaderFooterConstraints): st
if (margins.header !== undefined) {
parts.push(`mh:${margins.header}`);
}
if (margins.footer !== undefined) {
parts.push(`mf:${margins.footer}`);
}
}

return parts.join('|');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,19 @@ describe('Cache Invalidation', () => {
width: 500,
height: 100,
pageWidth: 600,
pageHeight: 900,
margins: { left: 50, right: 50, top: 72, bottom: 72, header: 36 },
pageHeight: 800,
margins: { left: 50, right: 50, top: 40, bottom: 60, header: 30, footer: 20 },
};

const hash = computeConstraintsHash(constraints);
expect(hash).toContain('pw:600');
expect(hash).toContain('ph:900');
expect(hash).toContain('ph:800');
expect(hash).toContain('ml:50');
expect(hash).toContain('mr:50');
expect(hash).toContain('mt:72');
expect(hash).toContain('mb:72');
expect(hash).toContain('mh:36');
expect(hash).toContain('mt:40');
expect(hash).toContain('mb:60');
expect(hash).toContain('mh:30');
expect(hash).toContain('mf:20');
});

it('should produce different hashes for different constraints', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/layout-engine/layout-engine/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type HeaderFooterConstraints = {
/**
* Page margins for anchor positioning.
* `left`/`right`: horizontal page-relative conversion.
* `top`/`bottom`: vertical margin-relative conversion and footer band origin.
* `top`/`bottom`: vertical margin-relative conversion and fallback footer band origin.
* `header`: header distance from page top edge (header band origin).
* `footer`: footer distance from page bottom edge (footer band origin).
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/layout-engine/layout-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ export type HeaderFooterConstraints = {
/**
* Page margins for anchor positioning.
* `left`/`right`: horizontal page-relative conversion.
* `top`/`bottom`: vertical margin-relative conversion and footer band origin.
* `top`/`bottom`: vertical margin-relative conversion and fallback footer band origin.
* `header`: header distance from page top edge (header band origin).
* `footer`: footer distance from page bottom edge (footer band origin).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ function makeDummyMeasure(): Measure {

const PAGE_HEIGHT = 1056;
const MARGIN_BOTTOM = 72;
const FOOTER_DISTANCE = 36;

const fullConstraints = {
pageHeight: PAGE_HEIGHT,
margins: { left: 72, right: 72, top: 72, bottom: MARGIN_BOTTOM, header: 36 },
margins: { left: 72, right: 72, top: 72, bottom: MARGIN_BOTTOM, header: 36, footer: FOOTER_DISTANCE },
};

const FOOTER_BAND_ORIGIN = PAGE_HEIGHT - MARGIN_BOTTOM; // 984
const FOOTER_BAND_ORIGIN = PAGE_HEIGHT - FOOTER_DISTANCE; // 1020

// ---------------------------------------------------------------------------
// Tests
Expand All @@ -46,7 +47,7 @@ describe('normalizeFragmentsForRegion (footer page-relative only)', () => {

normalizeFragmentsForRegion(pages, [block], [makeDummyMeasure()], 'footer', fullConstraints);

// physicalY = 0, bandOrigin = 984
// physicalY = 0, bandOrigin = 1020
expect(fragment.y).toBe(0 - FOOTER_BAND_ORIGIN);
});

Expand All @@ -63,7 +64,7 @@ describe('normalizeFragmentsForRegion (footer page-relative only)', () => {

normalizeFragmentsForRegion(pages, [block], [makeDummyMeasure()], 'footer', fullConstraints);

// physicalY = 1056 - 50 = 1006, bandOrigin = 984
// physicalY = 1056 - 50 = 1006, bandOrigin = 1020
expect(fragment.y).toBe(PAGE_HEIGHT - imgHeight - FOOTER_BAND_ORIGIN);
});

Expand All @@ -80,7 +81,7 @@ describe('normalizeFragmentsForRegion (footer page-relative only)', () => {

normalizeFragmentsForRegion(pages, [block], [makeDummyMeasure()], 'footer', fullConstraints);

// physicalY = (1056 - 40) / 2 = 508, bandOrigin = 984
// physicalY = (1056 - 40) / 2 = 508, bandOrigin = 1020
expect(fragment.y).toBe((PAGE_HEIGHT - imgHeight) / 2 - FOOTER_BAND_ORIGIN);
});

Expand All @@ -96,7 +97,7 @@ describe('normalizeFragmentsForRegion (footer page-relative only)', () => {

normalizeFragmentsForRegion(pages, [block], [makeDummyMeasure()], 'footer', fullConstraints);

// physicalY = 20, bandOrigin = 984
// physicalY = 20, bandOrigin = 1020
expect(fragment.y).toBe(20 - FOOTER_BAND_ORIGIN);
});

Expand All @@ -123,6 +124,28 @@ describe('normalizeFragmentsForRegion (footer page-relative only)', () => {

expect(fragment.y).toBe(PAGE_HEIGHT - 50 - FOOTER_BAND_ORIGIN);
});

it('falls back to bottom margin when footer distance is missing', () => {
const imgHeight = 40;
const block: FlowBlock = {
kind: 'image',
id: 'img-bottom',
src: 'test.png',
anchor: { isAnchored: true, vRelativeFrom: 'page', alignV: 'bottom', offsetV: 0 },
};
const fragment = makeAnchoredImageFragment('img-bottom', 0, imgHeight);
const pages = [{ number: 1, fragments: [fragment] }];

const withoutFooter = {
pageHeight: PAGE_HEIGHT,
margins: { left: 72, right: 72, top: 72, bottom: MARGIN_BOTTOM, header: 36 },
};

normalizeFragmentsForRegion(pages, [block], [makeDummyMeasure()], 'footer', withoutFooter);

const fallbackOrigin = PAGE_HEIGHT - MARGIN_BOTTOM;
expect(fragment.y).toBe(PAGE_HEIGHT - imgHeight - fallbackOrigin);
});
});

describe('passthrough cases — fragments that must NOT be modified', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type RegionConstraints = {
top?: number;
bottom?: number;
header?: number;
footer?: number;
};
};

Expand Down Expand Up @@ -49,7 +50,12 @@ function computePhysicalAnchorY(block: ImageBlock | DrawingBlock, fragmentHeight
* footer-local y=0. This is the top of the bottom margin area.
*/
function computeFooterBandOrigin(constraints: RegionConstraints): number {
Comment thread
caio-pizzol marked this conversation as resolved.
return (constraints.pageHeight ?? 0) - (constraints.margins?.bottom ?? 0);
const pageHeight = constraints.pageHeight ?? 0;
const footerDistance = constraints.margins?.footer;
if (typeof footerDistance === 'number' && Number.isFinite(footerDistance)) {
return Math.max(0, pageHeight - Math.max(0, footerDistance));
}
return Math.max(0, pageHeight - (constraints.margins?.bottom ?? 0));
}

function isAnchoredFragment(fragment: Fragment): boolean {
Expand Down
67 changes: 64 additions & 3 deletions packages/layout-engine/painters/dom/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4815,9 +4815,70 @@ describe('DomPainter', () => {
expect(footerFragmentEl).toBeTruthy();
// Footer container is at effectiveOffset (400px)
expect(footerEl.style.top).toBe(`${footerOffset}px`);
// Fragment uses band-local Y + container offset from band origin
// The exact top depends on getDecorationAnchorPageOriginY, but the
// key invariant is that the absolute page position is correct.
// Fragment uses band-local Y + container offset from band origin.
// With footer distance provided, anchors convert back to absolute page-space
// using the physical footer reference point (pageHeight - footerDistance).
const renderedPageTop = parseFloat(footerEl.style.top || '0') + parseFloat(footerFragmentEl.style.top || '0');
expect(renderedPageTop).toBe((layout.pageSize?.h ?? 0) - 20 + footerFragment.y);
});

it('falls back to bottom margin origin when footer distance is missing', () => {
const footerImageBlock: FlowBlock = {
kind: 'image',
id: 'footer-page-relative-img-missing',
src: 'data:image/png;base64,xxx',
anchor: {
isAnchored: true,
hRelativeFrom: 'page',
vRelativeFrom: 'page',
},
};
const footerImageMeasure: Measure = {
kind: 'image',
width: 20,
height: 20,
};
const footerOffset = 400;
const footerFragment: Fragment = {
kind: 'image',
blockId: footerImageBlock.id,
x: 0,
y: 25,
width: 20,
height: 20,
isAnchored: true,
};

const painter = createTestPainter({
blocks: [block, footerImageBlock],
measures: [measure, footerImageMeasure],
footerProvider: () => ({
fragments: [footerFragment],
height: 80,
offset: footerOffset,
}),
});

painter.paint(
{
...layout,
pages: [
{
...layout.pages[0],
number: 1,
margins: { left: 0, right: 0, bottom: 100 },
},
],
},
mount,
);

const footerEl = mount.querySelector('.superdoc-page-footer') as HTMLElement;
const footerFragmentEl = footerEl.querySelector(
'[data-block-id="footer-page-relative-img-missing"]',
) as HTMLElement;

expect(footerFragmentEl).toBeTruthy();
const renderedPageTop = parseFloat(footerEl.style.top || '0') + parseFloat(footerFragmentEl.style.top || '0');
expect(renderedPageTop).toBe(footerOffset + footerFragment.y);
});
Expand Down
16 changes: 11 additions & 5 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2398,18 +2398,24 @@ export class DomPainter {
}

const pageMargins = resolvedPage?.margins ?? page.margins;
const styledPageHeight = Number.parseFloat(pageEl.style.height || '');
const pageHeight =
page.size?.h ??
this.currentLayout?.pageSize?.h ??
(Number.isFinite(styledPageHeight) ? styledPageHeight : pageEl.clientHeight);

const footerDistance = pageMargins?.footer;
if (typeof footerDistance === 'number' && Number.isFinite(footerDistance)) {
return Math.max(0, pageHeight - Math.max(0, footerDistance));
}

const bottomMargin = pageMargins?.bottom;
if (bottomMargin == null) {
return effectiveOffset;
}

const footnoteReserve = resolvedPage?.footnoteReserved ?? page.footnoteReserved ?? 0;
const adjustedBottomMargin = Math.max(0, bottomMargin - footnoteReserve);
const styledPageHeight = Number.parseFloat(pageEl.style.height || '');
const pageHeight =
page.size?.h ??
this.currentLayout?.pageSize?.h ??
(Number.isFinite(styledPageHeight) ? styledPageHeight : pageEl.clientHeight);

return Math.max(0, pageHeight - adjustedBottomMargin);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7253,6 +7253,10 @@ export class PresentationEditor extends EventEmitter {
top: marginTop,
bottom: marginBottom,
header: headerMargin,
// Only set footer when the source defines w:footer. Defaulting to 0 here
// would defeat the bottom-margin fallback in computeFooterBandOrigin
// (typeof 0 === 'number' passes the check, returning pageHeight - 0).
...(margins.footer != null ? { footer: footerMargin } : {}),
},
overflowBaseHeight,
};
Expand Down
Loading