feat: add further page formats
This commit is contained in:
1 parent
bbd84cbeed
commit
b6556bf82a
7 files changed
+75
-20
No files matched your search
@@ -51,6 +51,7 @@
|
||||
|
||||
**Page & layout**
|
||||
- Page margins (cm) and page orientation (portrait / landscape)
|
||||
- Page format picker (Layout panel): 15 Word/LibreOffice sizes — A3–A6, ISO B4–B6, JIS B4/B5, Letter, Legal, Tabloid, Executive, Folio, Statement — each showing its cm dimensions; drives the on-screen page, pagination, and ODT/DOCX/PDF export, and is detected & adopted on import
|
||||
- Multi-column (newspaper) layout: 1–3 columns with adjustable gap, applied to the whole document (no selection) or to selected paragraphs, Word-style — text fills column 1 to the page bottom, then column 2, and flows across pages with mid-paragraph line breaks; a section followed by other content balances its columns. Round-trips to ODF `text:section`/`style:columns` and DOCX continuous sections with `w:cols`
|
||||
- Headers & footers with page-number / page-count fields and configurable edge distances
|
||||
- Zoom (20–300 %)
|
||||
|
||||
+12
-6
@@ -1258,13 +1258,21 @@ function rezipOdt(files: Record<string, Uint8Array>): Uint8Array {
|
||||
// Rewrite styles.xml to match the editor's preview: default font Liberation Serif →
|
||||
// Times New Roman (metric-identical; see EXPORT_FONT), and Heading_20_1/2/3 sizes &
|
||||
// margins → the editor's values (odf-kit's defaults are larger; HEADING_STYLE_OVERRIDES).
|
||||
function rewriteStylesXml(odtBytes: Uint8Array, lang: { language: string; country: string } | null): Uint8Array {
|
||||
function rewriteStylesXml(odtBytes: Uint8Array, lang: { language: string; country: string } | null, pageFormat: PageFormat, orientation: Orientation): Uint8Array {
|
||||
const files = unzipSync(odtBytes);
|
||||
const stylesBytes = files['styles.xml'];
|
||||
if (!stylesBytes) return odtBytes;
|
||||
|
||||
let styles = strFromU8(stylesBytes);
|
||||
|
||||
// Exact page box for the chosen format; odf-kit only emits its 5 presets, so
|
||||
// override the single fo:page-width/height in the page layout (round to 3 dp).
|
||||
const round3 = (v: number) => Math.round(v * 1000) / 1000;
|
||||
const dims = pageDimsCm(pageFormat, orientation);
|
||||
styles = styles
|
||||
.replace(/fo:page-width="[^"]*"/, `fo:page-width="${round3(dims.w)}cm"`)
|
||||
.replace(/fo:page-height="[^"]*"/, `fo:page-height="${round3(dims.h)}cm"`);
|
||||
|
||||
// Document spell-check language: set fo:language/fo:country on the base
|
||||
// Standard paragraph style, which every paragraph inherits from. LibreOffice
|
||||
// and Word read this as the document default language.
|
||||
@@ -1941,10 +1949,8 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
const cellBlocks: CellBlock[][] = [];
|
||||
|
||||
const odt = await tiptapToOdt(json, {
|
||||
// Format + orientation come from the Layout panel; odf-kit emits the preset's
|
||||
// fo:page-width/height and swaps them for landscape. Our explicit margins below
|
||||
// override the preset's default margins, so only the page size follows the format.
|
||||
pageFormat,
|
||||
// Orientation sets style:print-orientation; rewriteStylesXml overrides the exact
|
||||
// fo:page-width/height for the chosen format (odf-kit only knows 5 presets).
|
||||
orientation,
|
||||
// Margins (cm) from the Layout panel, matching the editor's padding so line
|
||||
// wrapping / page flow is identical. With a header/footer the vertical margin is
|
||||
@@ -2048,7 +2054,7 @@ export async function buildOdt(docJson: TiptapNode, margins: PageMargins = DEFAU
|
||||
const withToc = applyToc(withColumns, tocs, contentWidthCm);
|
||||
const withPageBreaks = applyPageBreaks(withToc);
|
||||
const withEmptyFontSizes = applyEmptyLineFontSizes(withPageBreaks);
|
||||
const withStyles = rewriteStylesXml(withEmptyFontSizes, language ?? null);
|
||||
const withStyles = rewriteStylesXml(withEmptyFontSizes, language ?? null, pageFormat, orientation);
|
||||
return applyHfPostProcess(withStyles, margins, headerPara, footerPara, headerDist, footerDist);
|
||||
}
|
||||
|
||||
|
||||
@@ -305,11 +305,11 @@ export async function printRaster(opts: PdfOptions): Promise<void> {
|
||||
}
|
||||
|
||||
const title = (opts.fileName ?? deriveFilename(opts.json)).replace(/\.(odt|pdf)$/i, '');
|
||||
const landscape = (opts.orientation ?? 'portrait') === 'landscape';
|
||||
// Explicit pt dimensions matching the @page box pin each image to exactly one page
|
||||
// (a hair of rounding overflow would otherwise spill a blank page after each).
|
||||
// Explicit cm size (orientation baked into w/h) works for any format, unlike a CSS
|
||||
// size keyword. Explicit pt image dims pin each image to exactly one page.
|
||||
const dims = pageDimsCm(opts.pageFormat ?? 'A4', opts.orientation ?? 'portrait');
|
||||
const css = `
|
||||
@page { size: ${opts.pageFormat ?? 'A4'} ${landscape ? 'landscape' : 'portrait'}; margin: 0; }
|
||||
@page { size: ${dims.w}cm ${dims.h}cm; margin: 0; }
|
||||
html, body { margin: 0; padding: 0; background: #fff; }
|
||||
img.pg { display: block; width: ${pageW * PT}pt; height: ${pageH * PT}pt; break-after: page; page-break-after: always; }
|
||||
img.pg:last-child { break-after: auto; page-break-after: auto; }
|
||||
@@ -433,7 +433,10 @@ function marginBoxes(headerDoc: HfDoc, footerDoc: HfDoc): string {
|
||||
|
||||
function printCss(o: PrintPdfOptions): string {
|
||||
const m = o.margins;
|
||||
const size = `${o.pageFormat ?? 'A4'} ${o.orientation === 'landscape' ? 'landscape' : 'portrait'}`;
|
||||
// Explicit cm size (orientation baked into w/h) works for any format, unlike a CSS
|
||||
// size keyword such as A4/letter.
|
||||
const d = pageDimsCm(o.pageFormat ?? 'A4', o.orientation);
|
||||
const size = `${d.w}cm ${d.h}cm`;
|
||||
return `
|
||||
@page {
|
||||
size: ${size};
|
||||
|
||||
@@ -97,7 +97,13 @@ const de: Messages = {
|
||||
pageLayoutMargins: 'Seitenlayout & Ränder',
|
||||
layout: 'Layout',
|
||||
pageFormat: 'Seitenformat',
|
||||
pageFormats: { A4: 'A4', letter: 'Letter', legal: 'Legal', A3: 'A3', A5: 'A5' },
|
||||
pageFormats: {
|
||||
A3: 'A3', A4: 'A4', A5: 'A5', A6: 'A6',
|
||||
isoB4: 'B4 (ISO)', isoB5: 'B5 (ISO)', isoB6: 'B6 (ISO)',
|
||||
jisB4: 'B4 (JIS)', jisB5: 'B5 (JIS)',
|
||||
letter: 'Letter', legal: 'Legal', tabloid: 'Tabloid',
|
||||
executive: 'Executive', folio: 'Folio', statement: 'Statement',
|
||||
},
|
||||
orientation: 'Ausrichtung',
|
||||
portrait: 'Hochformat',
|
||||
landscape: 'Querformat',
|
||||
|
||||
@@ -100,7 +100,13 @@ const en = {
|
||||
pageLayoutMargins: 'Page layout & margins',
|
||||
layout: 'Layout',
|
||||
pageFormat: 'Page format',
|
||||
pageFormats: { A4: 'A4', letter: 'Letter', legal: 'Legal', A3: 'A3', A5: 'A5' },
|
||||
pageFormats: {
|
||||
A3: 'A3', A4: 'A4', A5: 'A5', A6: 'A6',
|
||||
isoB4: 'B4 (ISO)', isoB5: 'B5 (ISO)', isoB6: 'B6 (ISO)',
|
||||
jisB4: 'B4 (JIS)', jisB5: 'B5 (JIS)',
|
||||
letter: 'Letter', legal: 'Legal', tabloid: 'Tabloid',
|
||||
executive: 'Executive', folio: 'Folio', statement: 'Statement',
|
||||
},
|
||||
orientation: 'Orientation',
|
||||
portrait: 'Portrait',
|
||||
landscape: 'Landscape',
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
import { PX_PER_CM } from './pageMargins';
|
||||
import type { Orientation } from './pageOrientation';
|
||||
|
||||
export type PageFormat = 'A4' | 'letter' | 'legal' | 'A3' | 'A5';
|
||||
export type PageFormat =
|
||||
| 'A3' | 'A4' | 'A5' | 'A6'
|
||||
| 'isoB4' | 'isoB5' | 'isoB6'
|
||||
| 'jisB4' | 'jisB5'
|
||||
| 'letter' | 'legal' | 'tabloid' | 'executive' | 'folio' | 'statement';
|
||||
|
||||
const KEY = 'odf-editor-page-format';
|
||||
|
||||
// Portrait page dimensions in cm per format (width < height). Landscape swaps them.
|
||||
// Matches odf-kit's pageFormat presets so ODT/DOCX round-trip is lossless.
|
||||
// Grouped A / ISO-B / JIS-B / US, matching the Word/LibreOffice paper menus.
|
||||
export const PAGE_FORMAT_CM: Record<PageFormat, { w: number; h: number }> = {
|
||||
A4: { w: 21, h: 29.7 },
|
||||
letter: { w: 21.59, h: 27.94 },
|
||||
legal: { w: 21.59, h: 35.56 },
|
||||
A3: { w: 29.7, h: 42 },
|
||||
A5: { w: 14.8, h: 21 },
|
||||
A3: { w: 29.7, h: 42 },
|
||||
A4: { w: 21, h: 29.7 },
|
||||
A5: { w: 14.8, h: 21 },
|
||||
A6: { w: 10.5, h: 14.8 },
|
||||
isoB4: { w: 25, h: 35.3 },
|
||||
isoB5: { w: 17.6, h: 25 },
|
||||
isoB6: { w: 12.5, h: 17.6 },
|
||||
jisB4: { w: 25.7, h: 36.4 },
|
||||
jisB5: { w: 18.2, h: 25.7 },
|
||||
letter: { w: 21.59, h: 27.94 },
|
||||
legal: { w: 21.59, h: 35.56 },
|
||||
tabloid: { w: 27.94, h: 43.18 },
|
||||
executive: { w: 18.415, h: 26.67 },
|
||||
folio: { w: 21.59, h: 33.02 },
|
||||
statement: { w: 13.97, h: 21.59 },
|
||||
};
|
||||
|
||||
const FORMATS = Object.keys(PAGE_FORMAT_CM) as PageFormat[];
|
||||
|
||||
@@ -317,6 +317,25 @@ describe('Leg 1a: page format', () => {
|
||||
const res = importOdt(await buildOdt(doc, margins, 'portrait', undefined, null, 'letter'));
|
||||
check('format round-trips as letter', res.format === 'letter', res.format);
|
||||
});
|
||||
|
||||
it('round-trips a non-preset format via the styles.xml override (tabloid)', async () => {
|
||||
const doc: N = { type: 'doc', content: [P(null, T('Tabloid page.'))] };
|
||||
const bytes = await buildOdt(doc, margins, 'portrait', undefined, null, 'tabloid');
|
||||
const styles = strFromU8(unzipSync(bytes)['styles.xml']);
|
||||
check('styles.xml emits tabloid width (27.94cm)', styles.includes('fo:page-width="27.94cm"'), styles.match(/fo:page-(width|height)="[^"]*"/g));
|
||||
check('styles.xml emits tabloid height (43.18cm)', styles.includes('fo:page-height="43.18cm"'));
|
||||
check('format round-trips as tabloid', importOdt(bytes).format === 'tabloid', importOdt(bytes).format);
|
||||
});
|
||||
|
||||
it('round-trips executive (fractional cm) and landscape swap', async () => {
|
||||
const doc: N = { type: 'doc', content: [P(null, T('Executive landscape.'))] };
|
||||
const bytes = await buildOdt(doc, margins, 'landscape', undefined, null, 'executive');
|
||||
const styles = strFromU8(unzipSync(bytes)['styles.xml']);
|
||||
check('landscape swaps executive width (26.67cm)', styles.includes('fo:page-width="26.67cm"'), styles.match(/fo:page-(width|height)="[^"]*"/g));
|
||||
const res = importOdt(bytes);
|
||||
check('format round-trips as executive', res.format === 'executive', res.format);
|
||||
check('orientation round-trips as landscape', res.orientation === 'landscape', res.orientation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leg 1b: merged table cells (colspan/rowspan)', () => {
|
||||
|
||||
Reference in new issue
Block a user