diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 8bf2251..06cc430 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,13 +4,14 @@
-
+
+
+
+
+
+
-
-
-
-
-
+
@@ -33,72 +34,72 @@
- {
+ "history": [
{
- "assignee": "iib0011"
+ "assignee": "iib0011"
},
{
- "state": "OPEN"
+ "state": "OPEN"
}
],
- "lastFilter": {
- "state": "OPEN"
+ "lastFilter": {
+ "state": "OPEN"
}
-}]]>
-
+ {
+ "prStates": [
{
- "id": {
- "id": "PR_kwDOMJIfts51PkS9",
- "number": 22
+ "id": {
+ "id": "PR_kwDOMJIfts51PkS9",
+ "number": 22
},
- "lastSeen": 1741207144695
+ "lastSeen": 1741207144695
},
{
- "id": {
- "id": "PR_kwDOMJIfts6NiNYl",
- "number": 32
+ "id": {
+ "id": "PR_kwDOMJIfts6NiNYl",
+ "number": 32
},
- "lastSeen": 1741209723869
+ "lastSeen": 1741209723869
},
{
- "id": {
- "id": "PR_kwDOMJIfts6Nheyd",
- "number": 31
+ "id": {
+ "id": "PR_kwDOMJIfts6Nheyd",
+ "number": 31
},
- "lastSeen": 1741213371410
+ "lastSeen": 1741213371410
},
{
- "id": {
- "id": "PR_kwDOMJIfts6NmRBs",
- "number": 33
+ "id": {
+ "id": "PR_kwDOMJIfts6NmRBs",
+ "number": 33
},
- "lastSeen": 1741282429036
+ "lastSeen": 1741282429036
},
{
- "id": {
- "id": "PR_kwDOMJIfts5zyFTs",
- "number": 15
+ "id": {
+ "id": "PR_kwDOMJIfts5zyFTs",
+ "number": 15
},
- "lastSeen": 1741535540953
+ "lastSeen": 1741535540953
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QQB3c",
- "number": 59
+ "id": {
+ "id": "PR_kwDOMJIfts6QQB3c",
+ "number": 59
},
- "lastSeen": 1743018960900
+ "lastSeen": 1743018960900
},
{
- "id": {
- "id": "PR_kwDOMJIfts6QMPEg",
- "number": 58
+ "id": {
+ "id": "PR_kwDOMJIfts6QMPEg",
+ "number": 58
},
- "lastSeen": 1743019452983
+ "lastSeen": 1743019452983
}
]
-}]]>
+}
{
"selectedUrlAndAccountId": {
"url": "https://github.com/iib0011/omni-tools.git",
@@ -127,55 +128,55 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "Docker.Dockerfile build.executor": "Run",
+ "Docker.Dockerfile.executor": "Run",
+ "Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
+ "Playwright.JoinText Component.executor": "Run",
+ "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "Vitest.compute function (1).executor": "Run",
+ "Vitest.compute function.executor": "Run",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "Vitest.parsePageRanges.executor": "Run",
+ "Vitest.removeDuplicateLines function.executor": "Run",
+ "Vitest.removeDuplicateLines function.newlines option.executor": "Run",
+ "Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
+ "Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
+ "Vitest.replaceText function.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/components/input",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.build.executor": "Run",
+ "npm.dev.executor": "Run",
+ "npm.lint.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "npm.test:e2e.executor": "Run",
+ "npm.test:e2e:run.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "refactai_advanced_settings",
+ "ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -806,7 +808,6 @@
-
@@ -831,7 +832,8 @@
-
+
+
diff --git a/src/pages/tools/pdf/index.ts b/src/pages/tools/pdf/index.ts
index e93a8c1..380e593 100644
--- a/src/pages/tools/pdf/index.ts
+++ b/src/pages/tools/pdf/index.ts
@@ -1,4 +1,5 @@
+import { tool as pdfRotatePdf } from './rotate-pdf/meta';
import { meta as splitPdfMeta } from './split-pdf/meta';
import { DefinedTool } from '@tools/defineTool';
-export const pdfTools: DefinedTool[] = [splitPdfMeta];
+export const pdfTools: DefinedTool[] = [splitPdfMeta, pdfRotatePdf];
diff --git a/src/pages/tools/pdf/rotate-pdf/index.tsx b/src/pages/tools/pdf/rotate-pdf/index.tsx
new file mode 100644
index 0000000..4152269
--- /dev/null
+++ b/src/pages/tools/pdf/rotate-pdf/index.tsx
@@ -0,0 +1,242 @@
+import { Box, FormControlLabel, Switch, Typography } from '@mui/material';
+import React, { useEffect, useState } from 'react';
+import ToolContent from '@components/ToolContent';
+import { ToolComponentProps } from '@tools/defineTool';
+import ToolPdfInput from '@components/input/ToolPdfInput';
+import ToolFileResult from '@components/result/ToolFileResult';
+import { CardExampleType } from '@components/examples/ToolExamples';
+import { PDFDocument } from 'pdf-lib';
+import { InitialValuesType, RotationAngle } from './types';
+import { parsePageRanges, rotatePdf } from './service';
+import SimpleRadio from '@components/options/SimpleRadio';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+
+const initialValues: InitialValuesType = {
+ rotationAngle: 90,
+ applyToAllPages: true,
+ pageRanges: ''
+};
+
+const exampleCards: CardExampleType[] = [
+ {
+ title: 'Rotate All Pages 90°',
+ description: 'Rotate all pages in the document 90 degrees clockwise',
+ sampleText: '',
+ sampleResult: '',
+ sampleOptions: {
+ rotationAngle: 90,
+ applyToAllPages: true,
+ pageRanges: ''
+ }
+ },
+ {
+ title: 'Rotate Specific Pages 180°',
+ description: 'Rotate only pages 1 and 3 by 180 degrees',
+ sampleText: '',
+ sampleResult: '',
+ sampleOptions: {
+ rotationAngle: 180,
+ applyToAllPages: false,
+ pageRanges: '1,3'
+ }
+ },
+ {
+ title: 'Rotate Page Range 270°',
+ description: 'Rotate pages 2 through 5 by 270 degrees',
+ sampleText: '',
+ sampleResult: '',
+ sampleOptions: {
+ rotationAngle: 270,
+ applyToAllPages: false,
+ pageRanges: '2-5'
+ }
+ }
+];
+
+export default function RotatePdf({
+ title,
+ longDescription
+}: ToolComponentProps) {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [totalPages, setTotalPages] = useState(0);
+ const [pageRangePreview, setPageRangePreview] = useState('');
+
+ // Get the total number of pages when a PDF is uploaded
+ useEffect(() => {
+ const getPdfInfo = async () => {
+ if (!input) {
+ setTotalPages(0);
+ return;
+ }
+
+ try {
+ const arrayBuffer = await input.arrayBuffer();
+ const pdf = await PDFDocument.load(arrayBuffer);
+ setTotalPages(pdf.getPageCount());
+ } catch (error) {
+ console.error('Error getting PDF info:', error);
+ setTotalPages(0);
+ }
+ };
+
+ getPdfInfo();
+ }, [input]);
+
+ const onValuesChange = (values: InitialValuesType) => {
+ const { pageRanges, applyToAllPages } = values;
+
+ if (applyToAllPages) {
+ setPageRangePreview(
+ totalPages > 0 ? `All ${totalPages} pages will be rotated` : ''
+ );
+ return;
+ }
+
+ if (!totalPages || !pageRanges?.trim()) {
+ setPageRangePreview('');
+ return;
+ }
+
+ try {
+ const count = parsePageRanges(pageRanges, totalPages).length;
+ setPageRangePreview(
+ `${count} page${count !== 1 ? 's' : ''} will be rotated`
+ );
+ } catch (error) {
+ setPageRangePreview('');
+ }
+ };
+
+ const compute = async (values: InitialValuesType, input: File | null) => {
+ if (!input) return;
+
+ try {
+ setIsProcessing(true);
+ const rotatedPdf = await rotatePdf(input, values);
+ setResult(rotatedPdf);
+ } catch (error) {
+ throw new Error('Error rotating PDF: ' + error);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+ const angleOptions: { value: RotationAngle; label: string }[] = [
+ { value: 90, label: '90° Clockwise' },
+ { value: 180, label: '180° (Upside down)' },
+ { value: 270, label: '270° (90° Counter-clockwise)' }
+ ];
+ return (
+
+ }
+ resultComponent={
+
+ }
+ getGroups={({ values, updateField }) => [
+ {
+ title: 'Rotation Settings',
+ component: (
+
+
+ Rotation Angle
+
+ {angleOptions.map((angleOption) => (
+ {
+ updateField('rotationAngle', angleOption.value);
+ }}
+ />
+ ))}
+
+
+ {
+ updateField('applyToAllPages', e.target.checked);
+ }}
+ />
+ }
+ label="Apply to all pages"
+ />
+
+
+ {!values.applyToAllPages && (
+
+ {totalPages > 0 && (
+
+ PDF has {totalPages} page{totalPages !== 1 ? 's' : ''}
+
+ )}
+ {
+ updateField('pageRanges', val);
+ }}
+ description={
+ 'Enter page numbers or ranges separated by commas (e.g., 1,3,5-7)'
+ }
+ placeholder={'e.g., 1,5-8'}
+ />
+ {pageRangePreview && (
+
+ {pageRangePreview}
+
+ )}
+
+ )}
+
+ )
+ }
+ ]}
+ onValuesChange={onValuesChange}
+ toolInfo={{
+ title: 'How to Use the Rotate PDF Tool',
+ description: `This tool allows you to rotate pages in a PDF document. You can rotate all pages or specify individual pages to rotate.
+
+Choose a rotation angle:
+- 90° Clockwise
+- 180° (Upside down)
+- 270° (90° Counter-clockwise)
+
+To rotate specific pages:
+1. Uncheck "Apply to all pages"
+2. Enter page numbers or ranges separated by commas (e.g., 1,3,5-7)
+
+Examples:
+- "1,5,9" rotates pages 1, 5, and 9
+- "1-5" rotates pages 1 through 5
+- "1,3-5,8-10" rotates pages 1, 3, 4, 5, 8, 9, and 10
+
+${longDescription}`
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/pdf/rotate-pdf/meta.ts b/src/pages/tools/pdf/rotate-pdf/meta.ts
new file mode 100644
index 0000000..8dc71a1
--- /dev/null
+++ b/src/pages/tools/pdf/rotate-pdf/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('pdf', {
+ name: 'Rotate PDF',
+ path: 'rotate-pdf',
+ icon: 'carbon:rotate',
+ description: 'Rotate PDF pages by 90, 180, or 270 degrees',
+ shortDescription: 'Rotate pages in a PDF document',
+ keywords: ['pdf', 'rotate', 'rotation', 'document', 'pages', 'orientation'],
+ longDescription:
+ 'Change the orientation of PDF pages by rotating them 90, 180, or 270 degrees. Useful for fixing incorrectly scanned documents or preparing PDFs for printing.',
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/pdf/rotate-pdf/rotate-pdf.service.test.ts b/src/pages/tools/pdf/rotate-pdf/rotate-pdf.service.test.ts
new file mode 100644
index 0000000..4fb2694
--- /dev/null
+++ b/src/pages/tools/pdf/rotate-pdf/rotate-pdf.service.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from 'vitest';
+import { parsePageRanges } from './service';
+
+describe('rotate-pdf', () => {
+ describe('parsePageRanges', () => {
+ it('should return all pages when pageRanges is empty', () => {
+ const result = parsePageRanges('', 5);
+ expect(result).toEqual([1, 2, 3, 4, 5]);
+ });
+
+ it('should parse single page numbers', () => {
+ const result = parsePageRanges('1,3,5', 5);
+ expect(result).toEqual([1, 3, 5]);
+ });
+
+ it('should parse page ranges', () => {
+ const result = parsePageRanges('2-4', 5);
+ expect(result).toEqual([2, 3, 4]);
+ });
+
+ it('should parse mixed page numbers and ranges', () => {
+ const result = parsePageRanges('1,3-5', 5);
+ expect(result).toEqual([1, 3, 4, 5]);
+ });
+
+ it('should ignore invalid page numbers', () => {
+ const result = parsePageRanges('1,8,3', 5);
+ expect(result).toEqual([1, 3]);
+ });
+
+ it('should handle whitespace', () => {
+ const result = parsePageRanges(' 1, 3 - 5 ', 5);
+ expect(result).toEqual([1, 3, 4, 5]);
+ });
+ });
+});
diff --git a/src/pages/tools/pdf/rotate-pdf/service.ts b/src/pages/tools/pdf/rotate-pdf/service.ts
new file mode 100644
index 0000000..ee72c63
--- /dev/null
+++ b/src/pages/tools/pdf/rotate-pdf/service.ts
@@ -0,0 +1,82 @@
+import { degrees, PDFDocument } from 'pdf-lib';
+import { InitialValuesType } from './types';
+
+/**
+ * Parses a page range string and returns an array of page numbers
+ * @param pageRangeStr String like "1,3-5,7" to extract pages 1, 3, 4, 5, and 7
+ * @param totalPages Total number of pages in the PDF
+ * @returns Array of page numbers to extract
+ */
+export function parsePageRanges(
+ pageRangeStr: string,
+ totalPages: number
+): number[] {
+ if (!pageRangeStr.trim()) {
+ return Array.from({ length: totalPages }, (_, i) => i + 1);
+ }
+
+ const pageNumbers = new Set();
+ const ranges = pageRangeStr.split(',');
+
+ for (const range of ranges) {
+ const trimmedRange = range.trim();
+
+ if (trimmedRange.includes('-')) {
+ const [start, end] = trimmedRange.split('-').map(Number);
+ if (!isNaN(start) && !isNaN(end)) {
+ // Handle both forward and reversed ranges
+ const normalizedStart = Math.min(start, end);
+ const normalizedEnd = Math.max(start, end);
+
+ for (
+ let i = Math.max(1, normalizedStart);
+ i <= Math.min(totalPages, normalizedEnd);
+ i++
+ ) {
+ pageNumbers.add(i);
+ }
+ }
+ } else {
+ const pageNum = parseInt(trimmedRange, 10);
+ if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
+ pageNumbers.add(pageNum);
+ }
+ }
+ }
+
+ return [...pageNumbers].sort((a, b) => a - b);
+}
+
+/**
+ * Rotates pages in a PDF file
+ * @param pdfFile The input PDF file
+ * @param options Options including rotation angle and page selection
+ * @returns Promise resolving to a new PDF file with rotated pages
+ */
+export async function rotatePdf(
+ pdfFile: File,
+ options: InitialValuesType
+): Promise {
+ const { rotationAngle, applyToAllPages, pageRanges } = options;
+
+ const arrayBuffer = await pdfFile.arrayBuffer();
+ const pdfDoc = await PDFDocument.load(arrayBuffer);
+ const totalPages = pdfDoc.getPageCount();
+
+ // Determine which pages to rotate
+ const pagesToRotate = applyToAllPages
+ ? Array.from({ length: totalPages }, (_, i) => i + 1)
+ : parsePageRanges(pageRanges, totalPages);
+
+ // Apply rotation to selected pages
+ for (const pageNum of pagesToRotate) {
+ const page = pdfDoc.getPage(pageNum - 1);
+ page.setRotation(degrees(rotationAngle));
+ }
+
+ // Save the modified PDF
+ const modifiedPdfBytes = await pdfDoc.save();
+ const newFileName = pdfFile.name.replace('.pdf', '-rotated.pdf');
+
+ return new File([modifiedPdfBytes], newFileName, { type: 'application/pdf' });
+}
diff --git a/src/pages/tools/pdf/rotate-pdf/types.ts b/src/pages/tools/pdf/rotate-pdf/types.ts
new file mode 100644
index 0000000..c32bbc5
--- /dev/null
+++ b/src/pages/tools/pdf/rotate-pdf/types.ts
@@ -0,0 +1,7 @@
+export type RotationAngle = 90 | 180 | 270;
+
+export type InitialValuesType = {
+ rotationAngle: RotationAngle;
+ applyToAllPages: boolean;
+ pageRanges: string;
+};