fix: a header/footer zone keeps its blank lines and its logo's height

A letterhead header is usually a couple of empty lines, the logo, then one
more — the empty leading ones were dropped on import, and the body's content
area counted every line as one text line, logo included. Page 1 of a contract
form started 19mm too high.
This commit is contained in:
Steffen Becker committed 2026-08-08 08:30:06 +00:00
1 parent d26fcae94d
commit f62559707c
2 files changed
+20 -7

No files matched your search

+10 -3
View File
@@ -66,9 +66,16 @@
const HF_LINE_PX = 16 * 1.15;
function hfReachPx(doc: HfDoc, distPx: number): number {
if (!doc || hfIsEmpty(doc)) return 0;
const inline = ((doc.content?.[0] as { content?: { type?: string }[] } | undefined)?.content ?? []);
const lines = 1 + inline.filter((n) => n.type === 'hardBreak').length;
return distPx + lines * HF_LINE_PX;
const inline = ((doc.content?.[0] as { content?: { type?: string; attrs?: { height?: number } }[] } | undefined)?.content ?? []);
// Per line, since an as-character image (a letterhead logo) makes its own line
// as tall as it is; the others are one text line each.
let total = 0;
let image = 0;
for (const n of inline) {
if (n.type === 'hardBreak') { total += Math.max(HF_LINE_PX, image); image = 0; }
else if (n.type === 'image' && typeof n.attrs?.height === 'number') image = Math.max(image, n.attrs.height);
}
return distPx + total + Math.max(HF_LINE_PX, image);
}
let footerDistPx = $derived(cmToPx((hfDistances ?? DEFAULT_HF_DISTANCES).footer));
let headerDistPx = $derived(cmToPx((hfDistances ?? DEFAULT_HF_DISTANCES).header));
+10 -4
View File
@@ -1613,11 +1613,12 @@ function convertHfPart(relId: string | null, ctx: Ctx): HfDoc {
const relsPath = path.replace(/^word\/(.*)$/, 'word/_rels/$1.rels');
const hfCtx: Ctx = { ...ctx, rels: parseRels(ctx.files[relsPath]) };
const inline: Node[] = [];
// One line per source paragraph, joined by hard breaks below: an empty leading line
// is a line of the zone too, and the body starts under the whole of it.
const lines: Node[][] = [];
let textAlign: string | null = null;
const boxMaps: Record<string, string>[] = [];
for (const p of hfParagraphs(root)) {
if (inline.length) inline.push({ type: 'hardBreak' });
const ppr = fc(p, 'pPr');
if (textAlign === null) {
const ta = (fc(ppr, 'jc') ? wVal(fc(ppr, 'jc')!) : null) ?? '';
@@ -1628,12 +1629,17 @@ function convertHfPart(relId: string | null, ctx: Ctx): HfDoc {
// The zone carries no styleName and no style CSS reaches it, so the yardstick is the
// editor's own defaults — what the Header/Footer style provides has to become marks
// (mirrors odt.ts convertHfZone, which passes no style name either).
inline.push(...convertInline(p, hfCtx, baseRun, blockDefaults({}, null, false), true).filter((n) => n.type !== PB_MARKER));
lines.push(convertInline(p, hfCtx, baseRun, blockDefaults({}, null, false), true).filter((n) => n.type !== PB_MARKER));
}
// An all-empty zone is dropped unless it carries a background/rule line (a footer that
// is just a colored line has no text). The zone collapses to one paragraph (mergeHfBox).
const box = mergeHfBox(boxMaps);
if (inline.length === 0 && Object.keys(box).length === 0) return null;
if (lines.every((l) => l.length === 0) && Object.keys(box).length === 0) return null;
const inline: Node[] = [];
lines.forEach((line, i) => {
if (i) inline.push({ type: 'hardBreak' });
inline.push(...line);
});
const para: Node = { type: 'paragraph', content: inline };
const attrs: Record<string, string> = {};