Enhance multi-page PDF layout with advanced customization options (#397, #3655) (#5859)

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
OUNZAR Aymane
2026-03-24 17:27:56 +00:00
committed by GitHub
co-authored by Copilot
parent 8bbfbd63d7
commit a1f03c844b
17 changed files with 1094 additions and 78 deletions
+112 -1
View File
@@ -4511,21 +4511,132 @@ title = "Extract Pages"
[pageLayout]
addBorder = "Add Borders"
borderWidth = "Border Thickness"
bottom = "Bottom Margin"
cols = "Columns"
header = "Multi Page Layout"
innerMargin = "Inner Margin"
left = "Left Margin"
pagesPerSheet = "Pages per sheet:"
right = "Right Margin"
rows = "Rows"
submit = "Submit"
tags = "merge,composite,single-view,organize"
title = "Multi Page Layout"
top = "Top Margin"
[pageLayout.mode]
default = "Default"
custom = "Custom"
label = "Mode:"
[pageLayout.arrangement]
byColumns = "By Columns"
byRows = "By Rows"
label = "Page arrangement:"
[pageLayout.desc]
16 = "Place 16 pages on a single sheet (4 × 4 grid)."
2 = "Place 2 pages side-by-side on a single sheet."
3 = "Place 3 pages on a single sheet in a single row."
4 = "Place 4 pages on a single sheet (2 × 2 grid)."
9 = "Place 9 pages on a single sheet (3 × 3 grid)."
[pageLayout.error]
failed = "An error occurred while creating the multi-page layout."
outerVerticalMarginsTooLarge = "Top/Bottom margins are too large for this page size."
outerHorizontalMarginsTooLarge = "Left/Right margins are too large for this page size."
innerMarginTooLarge = "Inner margin is too large for the selected layout."
[pageLayout.orientation]
landscape = "Landscape"
portrait = "Portrait"
label = "Orientation:"
[pageLayout.readingDirection]
ltr = "Left to Right"
rtl = "Right to Left"
label = "Reading Direction:"
[pageLayout.tooltip.header]
title = "Page Layout Guide"
[pageLayout.tooltip.overview]
title = "What is Page Layout?"
text = "Fit multiple pages onto a single sheet for handouts or to save paper."
[pageLayout.tooltip.mode]
title = "Mode"
text = "Choose how the grid is configured:"
bullet1 = "Default: Pick a preset and the grid is calculated automatically."
bullet2 = "Custom: Set rows and columns manually."
[pageLayout.tooltip.pagesPerSheet]
title = "Pages per Sheet (Default Mode)"
text = "Choose how many pages per sheet (e.g. 4 → 2×2, 9 → 3×3)."
[pageLayout.tooltip.rowsCols]
title = "Rows & Columns (Custom Mode)"
text = "Set exact grid dimensions. Total pages per sheet = rows × columns."
[pageLayout.tooltip.orientation]
title = "Orientation"
text = "Sets the output sheet orientation:"
bullet1 = "Portrait: Taller than wide."
bullet2 = "Landscape: Wider than tall."
[pageLayout.tooltip.arrangement]
title = "Page Arrangement"
text = "Controls the order pages fill the grid:"
bullet1 = "By Rows: Fill row by row (left-to-right or right-to-left)."
bullet2 = "By Columns: Fill top-to-bottom, column by column."
[pageLayout.tooltip.readingDirection]
title = "Reading Direction"
text = "Controls the horizontal order of pages:"
bullet1 = "LTR: Left to right."
bullet2 = "RTL: Right to left."
[pageLayout.tooltip.addBorder]
title = "Add Borders"
text = "Draws border lines around each page cell for cutting guides or visual separation."
[pageLayout.marginsBorders.tooltip.header]
title = "Margins and Borders"
[pageLayout.marginsBorders.tooltip.margins]
title = "Margins"
text = "Use top, bottom, left, and right margins to control spacing around the full sheet output."
[pageLayout.marginsBorders.tooltip.innerMargin]
title = "Inner Margin"
text = "Inner margin adds spacing between cells in the page grid to improve separation and readability."
[pageLayout.marginsBorders.tooltip.borders]
title = "Add Borders"
text = "Enable borders to draw lines around each placed page. This can help visual separation or trimming."
[pageLayout.marginsBorders.tooltip.borderWidth]
title = "Border Thickness"
text = "Border thickness is only applied when borders are enabled. Higher values produce thicker lines."
[pageLayout.advanced.tooltip.header]
title = "Advanced Layout Options"
[pageLayout.advanced.tooltip.orientation]
title = "Orientation"
text = "Choose the final sheet direction. Portrait works better for tall content, while landscape fits wider layouts."
[pageLayout.advanced.tooltip.arrangement]
title = "Page Arrangement"
text = "Controls whether pages fill the grid row-by-row or column-by-column."
bullet1 = "By Rows: Fill each row first."
bullet2 = "By Columns: Fill each column first."
[pageLayout.advanced.tooltip.readingDirection]
title = "Reading Direction"
text = "Sets horizontal ordering in the grid, useful for left-to-right and right-to-left document conventions."
bullet1 = "LTR: Left to right order."
bullet2 = "RTL: Right to left order."
[pageRemover]
header = "PDF Page remover"
@@ -0,0 +1,72 @@
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import { computeBoxes } from '@app/components/tools/pageLayout/utils/computeBoxes';
export default function LayoutPreview({
parameters
}: {
parameters: PageLayoutParameters
}) {
const sheet = {
x: 0,
y: 0,
width: parameters.orientation === "LANDSCAPE" ? 297 : 210,
height: parameters.orientation === "LANDSCAPE" ? 210 : 297
}
const boxes = computeBoxes(sheet, parameters);
const aspectRatio = sheet.width / sheet.height;
return (
<svg
viewBox={`${sheet.x} ${sheet.y} ${sheet.width} ${sheet.height}`}
preserveAspectRatio="xMidYMid meet"
style={{
width: parameters.orientation === "PORTRAIT" ? "50%" : `${50 * aspectRatio}%`,
aspectRatio: `${aspectRatio}`,
display: 'block',
margin: '0 auto',
padding: '10px',
}}>
<rect
x={sheet.x}
y={sheet.y}
width={sheet.width}
height={sheet.height}
rx={4}
ry={4}
fill="white"
stroke="black"
strokeWidth={2}
/>
{boxes.map(box => (
<g key={box.label}>
<rect
x={box.x}
y={box.y}
width={box.width}
height={box.height}
rx={3}
ry={3}
fill="#bfdbfe"
stroke={parameters.addBorder ? "#3b82f6" : "none"}
strokeWidth={2}
/>
<text
x={box.x + box.width / 2}
y={box.y + box.height / 2}
textAnchor="middle"
dominantBaseline="middle"
fontSize={Math.min(box.width, box.height) * 0.6}
fill="#1d4ed8"
fontFamily="Arial, sans-serif"
fontWeight="bold"
>
{box.label}
</text>
</g>))
}
</svg>
)
}
@@ -0,0 +1,75 @@
import { Divider, Select, Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
export default function PageLayoutAdvancedSettings({
parameters,
onParameterChange,
disabled,
}: {
parameters: PageLayoutParameters;
onParameterChange: <K extends keyof PageLayoutParameters>(
key: K,
value: PageLayoutParameters[K]
) => void;
disabled?: boolean;
}) {
const { t } = useTranslation();
return (
<Stack gap="sm">
<Select
label={t('pageLayout.orientation.label', 'Orientation:')}
data={[
{ value: 'PORTRAIT', label: t('pageLayout.orientation.portrait', 'Portrait') },
{ value: 'LANDSCAPE', label: t('pageLayout.orientation.landscape', 'Landscape') },
]}
value={String(parameters.orientation)}
onChange={(v) => {
if (v === "PORTRAIT" || v === "LANDSCAPE") {
onParameterChange('orientation', v)
}
}}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
<Divider />
<Select
label={t('pageLayout.arrangement.label', 'Page arrangement:')}
data={[
{ value: 'BY_ROWS', label: t('pageLayout.arrangement.byRows', 'By Rows') },
{ value: 'BY_COLUMNS', label: t('pageLayout.arrangement.byColumns', 'By Columns') },
]}
value={String(parameters.arrangement)}
onChange={(v) => {
if (v === "BY_COLUMNS" || v === "BY_ROWS") {
onParameterChange('arrangement', v)
}
}}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
<Select
label={t('pageLayout.readingDirection.label', 'Reading Direction:')}
data={[
{ value: 'LTR', label: t('pageLayout.readingDirection.ltr', 'Left to Right') },
{ value: 'RTL', label: t('pageLayout.readingDirection.rtl', 'Right to Left') },
]}
value={String(parameters.readingDirection)}
onChange={(v) => {
if (v === "LTR" || v === "RTL") {
onParameterChange('readingDirection', v)
}
}}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
</Stack>
);
}
@@ -0,0 +1,134 @@
import { Divider, Stack, NumberInput, Switch } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
export default function PageLayoutMarginsBordersSettings({
parameters,
onParameterChange,
disabled,
}: {
parameters: PageLayoutParameters;
onParameterChange: <K extends keyof PageLayoutParameters>(
key: K,
value: PageLayoutParameters[K]
) => void;
disabled?: boolean;
}) {
const { t } = useTranslation();
const cols = parameters.mode === "DEFAULT" ? Math.ceil(Math.sqrt(parameters.pagesPerSheet)) : parameters.cols;
const rows = parameters.mode === "DEFAULT" ? Math.ceil(parameters.pagesPerSheet / cols) : parameters.rows;
const left = parameters.leftMargin ?? 0;
const right = parameters.rightMargin ?? 0;
const top = parameters.topMargin ?? 0;
const bottom = parameters.bottomMargin ?? 0;
const inner = parameters.innerMargin ?? 0;
const pageWidth = parameters.orientation === "PORTRAIT"
? 595.28 // A4 width in points
: 841.89 ; // A4 height in points
const pageHeight = parameters.orientation === "PORTRAIT"
? 841.89 // A4 height in points
: 595.28; // A4 width in points
const cellWidth = (pageWidth - left - right) / cols;
const cellHeight = (pageHeight - top - bottom) / rows;
const innerWidth = cellWidth - 2 * inner;
const innerHeight = cellHeight - 2 * inner;
const invalidOuterWidth = left + right >= pageWidth;
const invalidOuterHeight = top + bottom >= pageHeight;
const invalidInnerSize = (innerWidth <= 0 || innerHeight <= 0) && inner > 0;
const outerHeightError = invalidOuterHeight
? t('pageLayout.error.outerVerticalMarginsTooLarge', 'Top/Bottom margins are too large for this page size.')
: undefined;
const outerWidthError = invalidOuterWidth
? t('pageLayout.error.outerHorizontalMarginsTooLarge', 'Left/Right margins are too large for this page size.')
: undefined;
const innerError = invalidInnerSize
? t('pageLayout.error.innerMarginTooLarge', 'Inner margin is too large for the selected layout.')
: undefined;
return (
<Stack gap="sm">
<NumberInput
label={t('pageLayout.top', 'Top Margin')}
placeholder="Enter top margin"
value={parameters.topMargin}
onChange={(v) => onParameterChange('topMargin', Number(v))}
min={0}
disabled={disabled}
style={{ flex: 1 }}
error={outerHeightError}
/>
<NumberInput
label={t('pageLayout.bottom', 'Bottom Margin')}
placeholder="Enter bottom margin"
value={parameters.bottomMargin}
onChange={(v) => onParameterChange('bottomMargin', Number(v))}
min={0}
disabled={disabled}
style={{ flex: 1 }}
error={outerHeightError}
/>
<NumberInput
label={t('pageLayout.left', 'Left Margin')}
placeholder="Enter left margin"
value={parameters.leftMargin}
onChange={(v) => onParameterChange('leftMargin', Number(v))}
min={0}
disabled={disabled}
error={outerWidthError}
/>
<NumberInput
label={t('pageLayout.right', 'Right Margin')}
placeholder="Enter right margin"
value={parameters.rightMargin}
onChange={(v) => onParameterChange('rightMargin', Number(v))}
min={0}
disabled={disabled}
style={{ flex: 1 }}
error={outerWidthError}
/>
<NumberInput
label={t('pageLayout.innerMargin', 'Inner Margin')}
placeholder="Enter inner margin"
value={parameters.innerMargin}
onChange={(v) => onParameterChange('innerMargin', Number(v))}
min={0}
disabled={disabled}
style={{ flex: 1 }}
error={innerError}
/>
<Divider />
<Switch
checked={parameters.addBorder}
onChange={(e) => onParameterChange('addBorder', e.currentTarget.checked)}
label={t('pageLayout.addBorder', 'Add Borders')}
disabled={disabled}
/>
{parameters.addBorder && (
<NumberInput
label={t('pageLayout.borderWidth', 'Border Thickness')}
placeholder="Enter border thickness"
value={parameters.borderWidth}
onChange={(v) => onParameterChange('borderWidth', Number(v))}
min={1}
disabled={disabled}
style={{ flex: 1 }}
/>
)}
</Stack>
);
}
@@ -0,0 +1,18 @@
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import LayoutPreview from '@app/components/tools/pageLayout/LayoutPreview';
import { Stack } from '@mantine/core';
export default function PageLayoutPreview({
parameters,
}: {
parameters: PageLayoutParameters;
}) {
return (
<Stack gap="sm">
<LayoutPreview parameters={parameters} />
</Stack>
);
}
@@ -1,8 +1,9 @@
import { Divider, Select, Stack, Switch } from '@mantine/core';
import { Select, Stack, NumberInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import { getPagesPerSheetOptions } from '@app/components/tools/pageLayout/constants';
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
import ButtonSelector from '@app/components/shared/ButtonSelector';
export default function PageLayoutSettings({
parameters,
@@ -18,44 +19,75 @@ export default function PageLayoutSettings({
}) {
const { t } = useTranslation();
const options = getPagesPerSheetOptions(t);
const selected = options.find((o) => o.value === parameters.pagesPerSheet) || options[0];
const pagesPerSheetOptions = getPagesPerSheetOptions(t);
const selectedPagesPerSheetOption = pagesPerSheetOptions.find((o) => o.value === parameters.pagesPerSheet) || pagesPerSheetOptions[0];
return (
<Stack gap="sm">
<Select
label={t('pageLayout.pagesPerSheet', 'Pages per sheet:')}
data={options.map(o => ({ value: String(o.value), label: o.label }))}
value={String(parameters.pagesPerSheet)}
onChange={(v) => onParameterChange('pagesPerSheet', Number(v))}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
{selected && (
<div
style={{
backgroundColor: 'var(--information-text-bg)',
color: 'var(--information-text-color)',
padding: '8px 12px',
borderRadius: '8px',
marginTop: '4px',
fontSize: '0.75rem',
textAlign: 'center',
}}
>
{selected.description}
</div>
)}
<Divider />
<Switch
checked={parameters.addBorder}
onChange={(e) => onParameterChange('addBorder', e.currentTarget.checked)}
label={t('pageLayout.addBorder', 'Add Borders')}
<ButtonSelector
label={t('pageLayout.mode.label', 'Mode:')}
options={[
{value: "DEFAULT", label: t('pageLayout.mode.default', 'Default')},
{value: "CUSTOM", label: t('pageLayout.mode.custom', 'Custom')}
]}
value={String(parameters.mode)}
onChange={(v) => {
if (v === "CUSTOM" || v === "DEFAULT") {
onParameterChange('mode', v)
}
}}
disabled={disabled}
/>
{parameters.mode === "DEFAULT" && <>
<Select
label={t('pageLayout.pagesPerSheet', 'Pages per sheet:')}
data={pagesPerSheetOptions.map(o => ({ value: String(o.value), label: o.label }))}
value={String(parameters.pagesPerSheet)}
onChange={(v) => onParameterChange('pagesPerSheet', Number(v))}
allowDeselect={false}
disabled={disabled}
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
/>
{selectedPagesPerSheetOption && (
<div
style={{
backgroundColor: 'var(--information-text-bg)',
color: 'var(--information-text-color)',
padding: '8px 12px',
borderRadius: '8px',
marginTop: '4px',
fontSize: '0.75rem',
textAlign: 'center',
}}
>
{selectedPagesPerSheetOption.description}
</div>
)}
</>}
{parameters.mode === "CUSTOM" && <>
<NumberInput
label={t('pageLayout.rows', 'Rows')}
placeholder="Enter rows"
value={parameters.rows}
onChange={(v) => onParameterChange('rows', Number(v))}
min={1}
disabled={disabled}
style={{ flex: 1 }}
/>
<NumberInput
label={t('pageLayout.cols', 'Columns')}
placeholder="Enter columns"
value={parameters.cols}
onChange={(v) => onParameterChange('cols', Number(v))}
min={1}
disabled={disabled}
style={{ flex: 1 }}
/>
</>}
</Stack>
);
}
@@ -12,11 +12,6 @@ export const getPagesPerSheetOptions = (t: TFunction): PagesPerSheetOption[] =>
label: '2',
description: t('pageLayout.desc.2', 'Place 2 pages side-by-side on a single sheet.')
},
{
value: 3,
label: '3',
description: t('pageLayout.desc.3', 'Place 3 pages on a single sheet in a single row.')
},
{
value: 4,
label: '4',
@@ -0,0 +1,62 @@
import { PageLayoutParameters } from "@app/hooks/tools/pageLayout/usePageLayoutParameters";
export type Box = {
x: number;
y: number;
width: number;
height: number;
label: number;
}
export type Sheet = {
x: number;
y: number;
width: number;
height: number;
}
export function computeBoxes(sheet: Sheet, parameters: PageLayoutParameters): Box[] {
const { mode, arrangement, readingDirection } = parameters;
const cols = mode === "DEFAULT" ? Math.ceil(Math.sqrt(parameters.pagesPerSheet)) : parameters.cols;
const rows = mode === "DEFAULT" ? Math.ceil(parameters.pagesPerSheet / cols) : parameters.rows;
const pagesPerSheet = mode === "DEFAULT" ? parameters.pagesPerSheet : cols * rows;
const boxes: Box[] = [];
const cellWidth = sheet.width / cols;
const cellHeight = sheet.height / rows;
const margin = Math.min(cellWidth, cellHeight) * 0.1;
for (let i = 0; i < pagesPerSheet; i++) {
let rowIndex, colIndex;
if ( arrangement === "BY_ROWS" ) {
rowIndex = Math.floor(i / cols);
if ( readingDirection === "LTR" ) {
colIndex = i % cols;
} else {
colIndex = cols - 1 - (i % cols);
}
} else {
rowIndex = i % rows;
if ( readingDirection === "LTR" ) {
colIndex = Math.floor(i / rows);
} else {
colIndex = cols - 1 - Math.floor(i / rows);
}
}
boxes.push({
x: sheet.x + colIndex * cellWidth + margin,
y: sheet.y + rowIndex * cellHeight + margin,
width: cellWidth - margin * 2,
height: cellHeight - margin * 2,
label: i + 1
});
}
return boxes;
}
@@ -0,0 +1,43 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '@app/types/tips';
export const usePageLayoutAdvancedTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t('pageLayout.advanced.tooltip.header.title', 'Advanced Layout Options'),
},
tips: [
{
title: t('pageLayout.advanced.tooltip.orientation.title', 'Orientation'),
description: t(
'pageLayout.advanced.tooltip.orientation.text',
'Choose the final sheet direction. Portrait works better for tall content, while landscape fits wider layouts.'
),
},
{
title: t('pageLayout.advanced.tooltip.arrangement.title', 'Page Arrangement'),
description: t(
'pageLayout.advanced.tooltip.arrangement.text',
'Controls whether pages fill the grid row-by-row or column-by-column.'
),
bullets: [
t('pageLayout.advanced.tooltip.arrangement.bullet1', 'By Rows: Fill each row first.'),
t('pageLayout.advanced.tooltip.arrangement.bullet2', 'By Columns: Fill each column first.'),
],
},
{
title: t('pageLayout.advanced.tooltip.readingDirection.title', 'Reading Direction'),
description: t(
'pageLayout.advanced.tooltip.readingDirection.text',
'Sets horizontal ordering in the grid, useful for left-to-right and right-to-left document conventions.'
),
bullets: [
t('pageLayout.advanced.tooltip.readingDirection.bullet1', 'LTR: Left to right order.'),
t('pageLayout.advanced.tooltip.readingDirection.bullet2', 'RTL: Right to left order.'),
],
},
],
};
};
@@ -0,0 +1,42 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '@app/types/tips';
export const usePageLayoutMarginsBordersTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t('pageLayout.marginsBorders.tooltip.header.title', 'Margins and Borders'),
},
tips: [
{
title: t('pageLayout.marginsBorders.tooltip.margins.title', 'Margins'),
description: t(
'pageLayout.marginsBorders.tooltip.margins.text',
'Use top, bottom, left, and right margins to control spacing around the full sheet output.'
),
},
{
title: t('pageLayout.marginsBorders.tooltip.innerMargin.title', 'Inner Margin'),
description: t(
'pageLayout.marginsBorders.tooltip.innerMargin.text',
'Inner margin adds spacing between cells in the page grid to improve separation and readability.'
),
},
{
title: t('pageLayout.marginsBorders.tooltip.borders.title', 'Add Borders'),
description: t(
'pageLayout.marginsBorders.tooltip.borders.text',
'Enable borders to draw lines around each placed page. This can help visual separation or trimming.'
),
},
{
title: t('pageLayout.marginsBorders.tooltip.borderWidth.title', 'Border Thickness'),
description: t(
'pageLayout.marginsBorders.tooltip.borderWidth.text',
'Border thickness is only applied when borders are enabled. Higher values produce thicker lines.'
),
},
],
};
};
@@ -0,0 +1,34 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '@app/types/tips';
export const usePageLayoutTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("pageLayout.tooltip.header.title", "Page Layout Guide")
},
tips: [
{
title: t("pageLayout.tooltip.overview.title", "What is Page Layout?"),
description: t("pageLayout.tooltip.overview.text", "Fit multiple pages onto a single sheet for handouts or to save paper.")
},
{
title: t("pageLayout.tooltip.mode.title", "Mode"),
description: t("pageLayout.tooltip.mode.text", "Choose how the grid is configured:"),
bullets: [
t("pageLayout.tooltip.mode.bullet1", "Default: Pick a preset and the grid is calculated automatically."),
t("pageLayout.tooltip.mode.bullet2", "Custom: Set rows and columns manually.")
]
},
{
title: t("pageLayout.tooltip.pagesPerSheet.title", "Pages per Sheet (Default Mode)"),
description: t("pageLayout.tooltip.pagesPerSheet.text", "Choose how many pages per sheet (e.g. 4 → 2×2, 9 → 3×3).")
},
{
title: t("pageLayout.tooltip.rowsCols.title", "Rows & Columns (Custom Mode)"),
description: t("pageLayout.tooltip.rowsCols.text", "Set exact grid dimensions. Total pages per sheet = rows × columns.")
}
]
};
};
@@ -6,8 +6,20 @@ import { PageLayoutParameters, defaultParameters } from '@app/hooks/tools/pageLa
export const buildPageLayoutFormData = (parameters: PageLayoutParameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
formData.append('mode', String(parameters.mode));
formData.append('pagesPerSheet', String(parameters.pagesPerSheet));
formData.append('rows', String(parameters.rows));
formData.append('cols', String(parameters.cols));
formData.append('orientation', String(parameters.orientation));
formData.append('arrangement', String(parameters.arrangement));
formData.append('readingDirection', String(parameters.readingDirection));
formData.append('innerMargin', String(parameters.innerMargin ?? 0));
formData.append('topMargin', String(parameters.topMargin ?? 0));
formData.append('bottomMargin', String(parameters.bottomMargin ?? 0));
formData.append('leftMargin', String(parameters.leftMargin ?? 0));
formData.append('rightMargin', String(parameters.rightMargin ?? 0));
formData.append('addBorder', String(parameters.addBorder));
formData.append('borderWidth', String(parameters.borderWidth ?? 1));
return formData;
};
@@ -2,13 +2,37 @@ import { BaseParameters } from '@app/types/parameters';
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
export interface PageLayoutParameters extends BaseParameters {
mode: 'DEFAULT' | 'CUSTOM';
pagesPerSheet: number;
rows: number;
cols: number;
orientation: 'PORTRAIT' | 'LANDSCAPE';
arrangement: 'BY_COLUMNS' | 'BY_ROWS';
readingDirection: 'LTR' | 'RTL';
innerMargin?: number;
topMargin?: number;
bottomMargin?: number;
leftMargin?: number;
rightMargin?: number;
addBorder: boolean;
borderWidth?: number;
}
export const defaultParameters: PageLayoutParameters = {
mode: 'DEFAULT',
pagesPerSheet: 4,
rows: 1,
cols: 1,
orientation: 'PORTRAIT',
arrangement: 'BY_COLUMNS',
readingDirection: 'LTR',
addBorder: false,
innerMargin: 0,
topMargin: 0,
bottomMargin: 0,
leftMargin: 0,
rightMargin: 0,
borderWidth: 1,
};
export type PageLayoutParametersHook = BaseParametersHook<PageLayoutParameters>;
@@ -17,6 +41,36 @@ export const usePageLayoutParameters = (): PageLayoutParametersHook => {
return useBaseParameters<PageLayoutParameters>({
defaultParameters,
endpointName: 'multi-page-layout',
validateFn: (params) => {
const cols = params.mode === 'DEFAULT'
? Math.ceil(Math.sqrt(params.pagesPerSheet))
: params.cols;
const rows = params.mode === 'DEFAULT'
? Math.ceil(params.pagesPerSheet / cols)
: params.rows;
if (cols <= 0 || rows <= 0) return false;
const pageWidth = params.orientation === 'PORTRAIT' ? 595.28 : 841.89;
const pageHeight = params.orientation === 'PORTRAIT' ? 841.89 : 595.28;
const left = params.leftMargin ?? 0;
const right = params.rightMargin ?? 0;
const top = params.topMargin ?? 0;
const bottom = params.bottomMargin ?? 0;
const inner = params.innerMargin ?? 0;
// Reject impossible outer margins first.
if (left + right >= pageWidth) return false;
if (top + bottom >= pageHeight) return false;
const cellWidth = (pageWidth - left - right) / cols;
const cellHeight = (pageHeight - top - bottom) / rows;
const innerWidth = cellWidth - 2 * inner;
const innerHeight = cellHeight - 2 * inner;
return innerWidth > 0 && innerHeight > 0;
},
});
};
+61 -3
View File
@@ -1,10 +1,24 @@
import { useTranslation } from 'react-i18next';
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
import { useAccordionSteps } from '@app/hooks/tools/shared/useAccordionSteps';
import { BaseToolProps, ToolComponent } from '@app/types/tool';
import { usePageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
import { usePageLayoutOperation } from '@app/hooks/tools/pageLayout/usePageLayoutOperation';
import PageLayoutSettings from '@app/components/tools/pageLayout/PageLayoutSettings';
import PageLayoutAdvancedSettings from '@app/components/tools/pageLayout/PageLayoutAdvancedSettings';
import PageLayoutMarginsBordersSettings from '@app/components/tools/pageLayout/PageLayoutMarginsBordersSettings';
import { usePageLayoutTips } from '@app/components/tooltips/PageLayout/usePageLayoutTips';
import { usePageLayoutAdvancedTips } from '@app/components/tooltips/PageLayout/usePageLayoutAdvancedTips';
import { usePageLayoutMarginsBordersTips } from '@app/components/tooltips/PageLayout/usePageLayoutMarginsBordersTips';
import PageLayoutPreview from '@app/components/tools/pageLayout/PageLayoutPreview';
enum PageLayoutStep {
NONE = 'none',
LAYOUT = 'layout',
ADVANCED = 'advanced',
MARGINS_BORDERS = 'marginsBorders',
}
const PageLayout = (props: BaseToolProps) => {
const { t } = useTranslation();
@@ -16,16 +30,34 @@ const PageLayout = (props: BaseToolProps) => {
props
);
const pageLayoutTips = usePageLayoutTips();
const pageLayoutAdvancedTips = usePageLayoutAdvancedTips();
const pageLayoutMarginsBordersTips = usePageLayoutMarginsBordersTips();
const accordion = useAccordionSteps<PageLayoutStep>({
noneValue: PageLayoutStep.NONE,
initialStep: PageLayoutStep.LAYOUT,
stateConditions: {
hasFiles: base.hasFiles,
hasResults: base.hasResults,
},
afterResults: base.handleSettingsReset,
});
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
preview: (
<PageLayoutPreview parameters={base.params.parameters} />
),
steps: [
{
title: 'Settings',
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
title: 'Layout settings',
isCollapsed: accordion.getCollapsedState(PageLayoutStep.LAYOUT),
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.LAYOUT),
tooltip: pageLayoutTips,
content: (
<PageLayoutSettings
parameters={base.params.parameters}
@@ -34,6 +66,32 @@ const PageLayout = (props: BaseToolProps) => {
/>
),
},
{
title: 'Advanced settings',
isCollapsed: accordion.getCollapsedState(PageLayoutStep.ADVANCED),
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.ADVANCED),
tooltip: pageLayoutAdvancedTips,
content: (
<PageLayoutAdvancedSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
{
title: 'Margins and borders',
isCollapsed: accordion.getCollapsedState(PageLayoutStep.MARGINS_BORDERS),
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.MARGINS_BORDERS),
tooltip: pageLayoutMarginsBordersTips,
content: (
<PageLayoutMarginsBordersSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t('pageLayout.submit', 'Create Layout'),