fix: pdf image list export

This commit is contained in:
Steffen Becker committed 2026-06-24 22:48:10 +02:00
1 parent a0d7ac8be3
commit 798af26456
1 file changed
+70
+70
View File
@@ -8,6 +8,7 @@ import { DEFAULT_MARGINS, type PageMargins } from '../storage/pageMargins';
import { hfIsEmpty, type HfDoc } from '../storage/headerFooter';
import { extensions } from '../editor/extensions';
import { columnPercents } from '../editor/tableView';
import { orderedTypeDef } from '../editor/orderedListTypes';
import { deriveFilename } from './odt';
import globalCss from '../../styles/global.css?inline';
import editorCss from '../../styles/editor.css?inline';
@@ -120,6 +121,74 @@ function collectRuns(root: HTMLElement): Run[] {
return runs;
}
const BULLET_CHARS = ['•', '◦', '▪']; // <ul> nesting cycle (disc/circle/square)
function toRoman(n: number): string {
const map: [number, string][] = [[1000, 'm'], [900, 'cm'], [500, 'd'], [400, 'cd'], [100, 'c'], [90, 'xc'], [50, 'l'], [40, 'xl'], [10, 'x'], [9, 'ix'], [5, 'v'], [4, 'iv'], [1, 'i']];
let s = '';
for (const [v, sym] of map) while (n >= v) { s += sym; n -= v; }
return s;
}
function toAlpha(n: number): string {
let s = '';
while (n > 0) { n--; s = String.fromCharCode(97 + (n % 26)) + s; n = Math.floor(n / 26); }
return s;
}
// The marker text the browser renders for a list item: the nesting-depth bullet for a
// <ul>, or the formatted ordinal (orderedListTypes) for an <ol>.
function listMarkerGlyph(li: HTMLElement, root: HTMLElement): string {
const parent = li.parentElement;
if (!parent) return '';
if (parent.tagName === 'OL') {
const items = Array.from(parent.children).filter((c) => c.tagName === 'LI');
const n = parseInt(parent.getAttribute('start') ?? '1', 10) + items.indexOf(li);
const def = orderedTypeDef(parent.getAttribute('data-list-style'));
const body = def.numFormat === 'a' ? toAlpha(n)
: def.numFormat === 'A' ? toAlpha(n).toUpperCase()
: def.numFormat === 'i' ? toRoman(n)
: def.numFormat === 'I' ? toRoman(n).toUpperCase()
: String(n);
return body + def.numSuffix;
}
let depth = 0;
for (let e: HTMLElement | null = li.parentElement; e && e !== root; e = e.parentElement) {
if (e.tagName === 'UL' || e.tagName === 'OL') depth++;
}
return BULLET_CHARS[(depth - 1 + BULLET_CHARS.length) % BULLET_CHARS.length];
}
// html2canvas paints list markers at the item's content-box left edge, ignoring floats, so
// a floating image that pushes an item's first line right strands the marker on the far side
// of the image. Re-hang such markers beside the shifted line (matching the editor's render).
function relocateFloatPushedMarkers(root: HTMLElement): void {
for (const li of Array.from(root.querySelectorAll<HTMLLIElement>('li'))) {
const cs = getComputedStyle(li);
if (cs.listStyleType === 'none' || cs.textAlign === 'center' || cs.textAlign === 'right') continue;
const p = li.querySelector(':scope > p');
if (!p) continue;
const range = document.createRange();
range.selectNodeContents(p);
const rects = Array.from(range.getClientRects()).filter((r) => r.width > 0 && r.height > 0);
if (!rects.length) continue;
const liRect = li.getBoundingClientRect();
const contentLeft = liRect.left + parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth);
// First line sits in the normal marker column → html2canvas already paints it right.
if (rects[0].left <= contentLeft + 6) continue;
const glyph = listMarkerGlyph(li, root);
if (!glyph) continue;
li.style.listStyleType = 'none';
const slot = document.createElement('span');
slot.style.cssText = 'display:inline-block;width:0;overflow:visible;vertical-align:baseline';
const label = document.createElement('span');
label.style.cssText = `display:inline-block;white-space:pre;transform:translateX(-100%);padding-right:0.4em;color:${cs.color}`;
label.textContent = glyph;
slot.appendChild(label);
p.insertBefore(slot, p.firstChild);
}
}
// Render the document to A4 pages (raster + invisible text) and download the PDF.
export async function exportPdf(opts: PdfOptions): Promise<void> {
const paper = opts.source.closest('.paper') as HTMLElement | null;
@@ -134,6 +203,7 @@ export async function exportPdf(opts: PdfOptions): Promise<void> {
try {
await document.fonts.ready;
relocateFloatPushedMarkers(clone);
const pages = Math.max(1, opts.numPages ?? Math.round((clone.offsetHeight + PAGE_GAP) / cycle));
const runs = collectRuns(clone);