mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Add landscape page size options (#6248)
# Description of Changes Adds orientation (portrait/landscape) to the Adjust Page Scale tool. - Orientation as a separate parameter (per review), sent through to the backend - ScalePagesController simplified; PDFWithPageSize gains the orientation field - Regenerated tool_models.py; frontend + backend tests added --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --------- Co-authored-by: ConnorYoh <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
parent
b3c4b8b463
commit
8bd78d2624
+11
-42
@@ -40,7 +40,8 @@ public class ScalePagesController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
private static PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
|
||||
private static PDRectangle getTargetSize(
|
||||
String targetPDRectangle, String orientation, PDDocument sourceDocument) {
|
||||
if ("KEEP".equals(targetPDRectangle)) {
|
||||
if (sourceDocument.getNumberOfPages() == 0) {
|
||||
throw ExceptionUtils.createInvalidPageSizeException("KEEP");
|
||||
@@ -57,18 +58,19 @@ public class ScalePagesController {
|
||||
}
|
||||
|
||||
Map<String, PDRectangle> sizeMap = getSizeMap();
|
||||
|
||||
if (sizeMap.containsKey(targetPDRectangle)) {
|
||||
return sizeMap.get(targetPDRectangle);
|
||||
PDRectangle base = sizeMap.get(targetPDRectangle);
|
||||
if (base == null) {
|
||||
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
|
||||
}
|
||||
|
||||
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
|
||||
if ("LANDSCAPE".equalsIgnoreCase(orientation)) {
|
||||
return new PDRectangle(base.getHeight(), base.getWidth());
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
private static Map<String, PDRectangle> getSizeMap() {
|
||||
Map<String, PDRectangle> sizeMap = new HashMap<>();
|
||||
|
||||
// Portrait sizes (A0-A6)
|
||||
sizeMap.put("A0", PDRectangle.A0);
|
||||
sizeMap.put("A1", PDRectangle.A1);
|
||||
sizeMap.put("A2", PDRectangle.A2);
|
||||
@@ -76,42 +78,8 @@ public class ScalePagesController {
|
||||
sizeMap.put("A4", PDRectangle.A4);
|
||||
sizeMap.put("A5", PDRectangle.A5);
|
||||
sizeMap.put("A6", PDRectangle.A6);
|
||||
|
||||
// Landscape sizes (A0-A6)
|
||||
sizeMap.put(
|
||||
"A0_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A0.getHeight(), PDRectangle.A0.getWidth()));
|
||||
sizeMap.put(
|
||||
"A1_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A1.getHeight(), PDRectangle.A1.getWidth()));
|
||||
sizeMap.put(
|
||||
"A2_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A2.getHeight(), PDRectangle.A2.getWidth()));
|
||||
sizeMap.put(
|
||||
"A3_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A3.getHeight(), PDRectangle.A3.getWidth()));
|
||||
sizeMap.put(
|
||||
"A4_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
|
||||
sizeMap.put(
|
||||
"A5_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A5.getHeight(), PDRectangle.A5.getWidth()));
|
||||
sizeMap.put(
|
||||
"A6_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A6.getHeight(), PDRectangle.A6.getWidth()));
|
||||
|
||||
// Portrait US sizes
|
||||
sizeMap.put("LETTER", PDRectangle.LETTER);
|
||||
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
||||
|
||||
// Landscape US sizes
|
||||
sizeMap.put(
|
||||
"LETTER_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.LETTER.getHeight(), PDRectangle.LETTER.getWidth()));
|
||||
sizeMap.put(
|
||||
"LEGAL_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.LEGAL.getHeight(), PDRectangle.LEGAL.getWidth()));
|
||||
|
||||
return sizeMap;
|
||||
}
|
||||
|
||||
@@ -128,13 +96,14 @@ public class ScalePagesController {
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String targetPDRectangle = request.getPageSize();
|
||||
String orientation = request.getOrientation();
|
||||
float scaleFactor = request.getScaleFactor();
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||
PDDocument outputDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
|
||||
PDRectangle targetSize = getTargetSize(targetPDRectangle, sourceDocument);
|
||||
PDRectangle targetSize = getTargetSize(targetPDRectangle, orientation, sourceDocument);
|
||||
|
||||
// Create LayerUtility once outside the loop for better performance
|
||||
LayerUtility layerUtility = new LayerUtility(outputDocument);
|
||||
|
||||
@@ -18,4 +18,11 @@ public class PDFWithPageSize extends PDFFile {
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL", "KEEP"})
|
||||
private String pageSize;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Orientation to apply to the target page size. Ignored when pageSize is KEEP.",
|
||||
defaultValue = "PORTRAIT",
|
||||
allowableValues = {"PORTRAIT", "LANDSCAPE"})
|
||||
private String orientation = "PORTRAIT";
|
||||
}
|
||||
|
||||
+2
-1
@@ -237,7 +237,8 @@ class ScalePagesControllerTest {
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A4_LANDSCAPE");
|
||||
request.setPageSize("A4");
|
||||
request.setOrientation("LANDSCAPE");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
@@ -1094,6 +1094,15 @@ class SanitizePdfParams(ApiModel):
|
||||
remove_xmp_metadata: bool = Field(False, description="Remove XMP metadata from the PDF")
|
||||
|
||||
|
||||
class Orientation1(StrEnum):
|
||||
"""
|
||||
Orientation to apply to the target page size. Ignored when pageSize is KEEP.
|
||||
"""
|
||||
|
||||
portrait = "PORTRAIT"
|
||||
landscape = "LANDSCAPE"
|
||||
|
||||
|
||||
class PageSize(StrEnum):
|
||||
"""
|
||||
The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP.
|
||||
@@ -1112,6 +1121,10 @@ class PageSize(StrEnum):
|
||||
|
||||
|
||||
class ScalePagesParams(ApiModel):
|
||||
orientation: Orientation1 = Field(
|
||||
Orientation1.portrait,
|
||||
description="Orientation to apply to the target page size. Ignored when pageSize is KEEP.",
|
||||
)
|
||||
page_size: PageSize = Field(
|
||||
..., description="The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL, KEEP."
|
||||
)
|
||||
|
||||
@@ -494,6 +494,11 @@ title = "Adjust Page Scale"
|
||||
[adjustPageScale.error]
|
||||
failed = "An error occurred while adjusting the page scale."
|
||||
|
||||
[adjustPageScale.orientation]
|
||||
label = "Page orientation"
|
||||
landscape = "Landscape"
|
||||
portrait = "Portrait"
|
||||
|
||||
[adjustPageScale.pageSize]
|
||||
keep = "Keep Original Size"
|
||||
label = "Target Page Size"
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ describe("AdjustPageScaleSettings", () => {
|
||||
const defaultParameters: AdjustPageScaleParameters = {
|
||||
scaleFactor: 1.0,
|
||||
pageSize: PageSize.KEEP,
|
||||
orientation: "PORTRAIT",
|
||||
};
|
||||
|
||||
const mockOnParameterChange = vi.fn();
|
||||
@@ -51,6 +52,7 @@ describe("AdjustPageScaleSettings", () => {
|
||||
const customParameters: AdjustPageScaleParameters = {
|
||||
scaleFactor: 2.5,
|
||||
pageSize: PageSize.A4,
|
||||
orientation: "LANDSCAPE",
|
||||
};
|
||||
|
||||
render(
|
||||
|
||||
+30
-4
@@ -1,7 +1,8 @@
|
||||
import { Stack, NumberInput, Select } from "@mantine/core";
|
||||
import { Stack, NumberInput, Select, SegmentedControl } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AdjustPageScaleParameters,
|
||||
Orientation,
|
||||
PageSize,
|
||||
} from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
|
||||
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||
@@ -22,6 +23,8 @@ const AdjustPageScaleSettings = ({
|
||||
}: AdjustPageScaleSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isKeepSelected = parameters.pageSize === PageSize.KEEP;
|
||||
|
||||
const pageSizeOptions = [
|
||||
{
|
||||
value: PageSize.KEEP,
|
||||
@@ -44,6 +47,17 @@ const AdjustPageScaleSettings = ({
|
||||
},
|
||||
];
|
||||
|
||||
const orientationOptions = [
|
||||
{
|
||||
value: "PORTRAIT",
|
||||
label: t("adjustPageScale.orientation.portrait", "Portrait"),
|
||||
},
|
||||
{
|
||||
value: "LANDSCAPE",
|
||||
label: t("adjustPageScale.orientation.landscape", "Landscape"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<NumberInput
|
||||
@@ -66,9 +80,10 @@ const AdjustPageScaleSettings = ({
|
||||
label={t("adjustPageScale.pageSize.label", "Target Page Size")}
|
||||
value={parameters.pageSize}
|
||||
onChange={(value) => {
|
||||
if (value && Object.values(PageSize).includes(value as PageSize)) {
|
||||
onParameterChange("pageSize", value as PageSize);
|
||||
}
|
||||
if (!value) return;
|
||||
const next = value as PageSize;
|
||||
if (!Object.values(PageSize).includes(next)) return;
|
||||
onParameterChange("pageSize", next);
|
||||
}}
|
||||
data={pageSizeOptions}
|
||||
disabled={disabled}
|
||||
@@ -77,6 +92,17 @@ const AdjustPageScaleSettings = ({
|
||||
zIndex: Z_INDEX_AUTOMATE_DROPDOWN,
|
||||
}}
|
||||
/>
|
||||
|
||||
<SegmentedControl
|
||||
aria-label={t("adjustPageScale.orientation.label", "Page orientation")}
|
||||
value={parameters.orientation}
|
||||
onChange={(value) =>
|
||||
onParameterChange("orientation", value as Orientation)
|
||||
}
|
||||
data={orientationOptions}
|
||||
disabled={disabled || isKeepSelected}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AdjustPageScaleParameters } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
|
||||
|
||||
export const buildAdjustPageScaleFormData = (
|
||||
parameters: AdjustPageScaleParameters,
|
||||
file: File,
|
||||
): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("scaleFactor", parameters.scaleFactor.toString());
|
||||
formData.append("pageSize", parameters.pageSize);
|
||||
formData.append("orientation", parameters.orientation);
|
||||
return formData;
|
||||
};
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildAdjustPageScaleFormData } from "@app/hooks/tools/adjustPageScale/adjustPageScaleFormData";
|
||||
import {
|
||||
defaultParameters,
|
||||
PageSize,
|
||||
} from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
|
||||
|
||||
const stubFile = () =>
|
||||
new File(["data"], "doc.pdf", { type: "application/pdf" });
|
||||
|
||||
describe("buildAdjustPageScaleFormData", () => {
|
||||
it("includes scaleFactor, pageSize, and orientation by default", () => {
|
||||
const formData = buildAdjustPageScaleFormData(
|
||||
defaultParameters,
|
||||
stubFile(),
|
||||
);
|
||||
expect(formData.get("scaleFactor")).toBe(
|
||||
defaultParameters.scaleFactor.toString(),
|
||||
);
|
||||
expect(formData.get("pageSize")).toBe(defaultParameters.pageSize);
|
||||
expect(formData.get("orientation")).toBe(defaultParameters.orientation);
|
||||
});
|
||||
|
||||
it("forwards orientation=LANDSCAPE to the backend", () => {
|
||||
const formData = buildAdjustPageScaleFormData(
|
||||
{ ...defaultParameters, pageSize: PageSize.A4, orientation: "LANDSCAPE" },
|
||||
stubFile(),
|
||||
);
|
||||
expect(formData.get("orientation")).toBe("LANDSCAPE");
|
||||
expect(formData.get("pageSize")).toBe("A4");
|
||||
});
|
||||
|
||||
it("forwards orientation=PORTRAIT to the backend", () => {
|
||||
const formData = buildAdjustPageScaleFormData(
|
||||
{
|
||||
...defaultParameters,
|
||||
pageSize: PageSize.LEGAL,
|
||||
orientation: "PORTRAIT",
|
||||
},
|
||||
stubFile(),
|
||||
);
|
||||
expect(formData.get("orientation")).toBe("PORTRAIT");
|
||||
expect(formData.get("pageSize")).toBe("LEGAL");
|
||||
});
|
||||
|
||||
it("attaches the uploaded file under fileInput", () => {
|
||||
const file = stubFile();
|
||||
const formData = buildAdjustPageScaleFormData(defaultParameters, file);
|
||||
expect(formData.get("fileInput")).toBe(file);
|
||||
});
|
||||
});
|
||||
+2
-10
@@ -8,17 +8,9 @@ import {
|
||||
AdjustPageScaleParameters,
|
||||
defaultParameters,
|
||||
} from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
|
||||
import { buildAdjustPageScaleFormData } from "@app/hooks/tools/adjustPageScale/adjustPageScaleFormData";
|
||||
|
||||
export const buildAdjustPageScaleFormData = (
|
||||
parameters: AdjustPageScaleParameters,
|
||||
file: File,
|
||||
): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("scaleFactor", parameters.scaleFactor.toString());
|
||||
formData.append("pageSize", parameters.pageSize);
|
||||
return formData;
|
||||
};
|
||||
export { buildAdjustPageScaleFormData };
|
||||
|
||||
export const adjustPageScaleOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
|
||||
+2
@@ -23,6 +23,8 @@ describe("useAdjustPageScaleParameters", () => {
|
||||
{ paramName: "pageSize" as const, value: PageSize.A4 },
|
||||
{ paramName: "pageSize" as const, value: PageSize.LETTER },
|
||||
{ paramName: "pageSize" as const, value: PageSize.LEGAL },
|
||||
{ paramName: "orientation" as const, value: "LANDSCAPE" as const },
|
||||
{ paramName: "orientation" as const, value: "PORTRAIT" as const },
|
||||
])("should update parameter $paramName to $value", ({ paramName, value }) => {
|
||||
const { result } = renderHook(() => useAdjustPageScaleParameters());
|
||||
|
||||
|
||||
@@ -17,14 +17,18 @@ export enum PageSize {
|
||||
LEGAL = "LEGAL",
|
||||
}
|
||||
|
||||
export type Orientation = "PORTRAIT" | "LANDSCAPE";
|
||||
|
||||
export interface AdjustPageScaleParameters extends BaseParameters {
|
||||
scaleFactor: number;
|
||||
pageSize: PageSize;
|
||||
orientation: Orientation;
|
||||
}
|
||||
|
||||
export const defaultParameters: AdjustPageScaleParameters = {
|
||||
scaleFactor: 1.0,
|
||||
pageSize: PageSize.KEEP,
|
||||
orientation: "PORTRAIT",
|
||||
};
|
||||
|
||||
export type AdjustPageScaleParametersHook =
|
||||
|
||||
Reference in New Issue
Block a user