mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-22 19:46:39 +00:00
Compare commits
5 Commits
1fa6b478bc
...
33f8172d5d
Author | SHA1 | Date | |
---|---|---|---|
![]() |
33f8172d5d | ||
![]() |
7dc01d9885 | ||
![]() |
61812bcbf3 | ||
![]() |
885e9cb119 | ||
![]() |
f6df414425 |
@ -19,8 +19,8 @@ const viewOptionStyle = {
|
||||
|
||||
|
||||
// Build view options showing text always
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [
|
||||
{
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
|
||||
const viewerOption = {
|
||||
label: (
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{switchingTo === "viewer" ? (
|
||||
@ -32,8 +32,9 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
</div>
|
||||
),
|
||||
value: "viewer",
|
||||
},
|
||||
{
|
||||
};
|
||||
|
||||
const pageEditorOption = {
|
||||
label: (
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "pageEditor" ? (
|
||||
@ -50,8 +51,9 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
</div>
|
||||
),
|
||||
value: "pageEditor",
|
||||
},
|
||||
{
|
||||
};
|
||||
|
||||
const fileEditorOption = {
|
||||
label: (
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "fileEditor" ? (
|
||||
@ -68,8 +70,15 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
</div>
|
||||
),
|
||||
value: "fileEditor",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// Build options array conditionally
|
||||
return [
|
||||
viewerOption,
|
||||
...(isToolSelected ? [] : [pageEditorOption]),
|
||||
fileEditorOption,
|
||||
];
|
||||
};
|
||||
|
||||
interface TopControlsProps {
|
||||
currentView: WorkbenchType;
|
||||
@ -91,7 +100,7 @@ const TopControls = ({
|
||||
if (!isValidWorkbench(view)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const workbench = view;
|
||||
|
||||
// Show immediate feedback
|
||||
@ -111,39 +120,37 @@ const TopControls = ({
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||
{!isToolSelected && (
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data={createViewOptions(currentView, switchingTo)}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
color="blue"
|
||||
fullWidth
|
||||
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
|
||||
style={{
|
||||
transition: 'all 0.2s ease',
|
||||
opacity: switchingTo ? 0.8 : 1,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 9999,
|
||||
maxHeight: '2.6rem',
|
||||
},
|
||||
control: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
indicator: {
|
||||
borderRadius: 9999,
|
||||
maxHeight: '2rem',
|
||||
},
|
||||
label: {
|
||||
paddingTop: '0rem',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data={createViewOptions(currentView, switchingTo, isToolSelected)}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
color="blue"
|
||||
fullWidth
|
||||
className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''}
|
||||
style={{
|
||||
transition: 'all 0.2s ease',
|
||||
opacity: switchingTo ? 0.8 : 1,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 9999,
|
||||
maxHeight: '2.6rem',
|
||||
},
|
||||
control: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
indicator: {
|
||||
borderRadius: 9999,
|
||||
maxHeight: '2rem',
|
||||
},
|
||||
label: {
|
||||
paddingTop: '0rem',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,29 +2,27 @@ import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { Box, useMantineTheme, MantineTheme } from '@mantine/core';
|
||||
import {
|
||||
PDFBounds,
|
||||
CropArea,
|
||||
DOMRect,
|
||||
Rectangle,
|
||||
domToPDFCoordinates,
|
||||
pdfToDOMCoordinates,
|
||||
constrainDOMRectToThumbnail,
|
||||
isPointInThumbnail
|
||||
} from '../../../utils/cropCoordinates';
|
||||
import { type ResizeHandle } from '../../../constants/cropConstants';
|
||||
|
||||
interface CropAreaSelectorProps {
|
||||
/** PDF bounds for coordinate conversion */
|
||||
pdfBounds: PDFBounds;
|
||||
/** Current crop area in PDF coordinates */
|
||||
cropArea: CropArea;
|
||||
cropArea: Rectangle;
|
||||
/** Callback when crop area changes */
|
||||
onCropAreaChange: (cropArea: CropArea) => void;
|
||||
onCropAreaChange: (cropArea: Rectangle) => void;
|
||||
/** Whether the selector is disabled */
|
||||
disabled?: boolean;
|
||||
/** Child content (typically the PDF thumbnail) */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;
|
||||
|
||||
const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
pdfBounds,
|
||||
cropArea,
|
||||
@ -39,7 +37,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isResizing, setIsResizing] = useState<ResizeHandle>(null);
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [initialCropArea, setInitialCropArea] = useState<CropArea>(cropArea);
|
||||
const [initialCropArea, setInitialCropArea] = useState<Rectangle>(cropArea);
|
||||
|
||||
// Convert PDF crop area to DOM coordinates for display
|
||||
const domRect = pdfToDOMCoordinates(cropArea, pdfBounds);
|
||||
@ -85,7 +83,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
// Start new crop selection
|
||||
const newDomRect: DOMRect = { x, y, width: 20, height: 20 };
|
||||
const newDomRect: Rectangle = { x, y, width: 20, height: 20 };
|
||||
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
||||
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
||||
|
||||
@ -107,7 +105,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
const newX = x - dragStart.x;
|
||||
const newY = y - dragStart.y;
|
||||
|
||||
const newDomRect: DOMRect = {
|
||||
const newDomRect: Rectangle = {
|
||||
x: newX,
|
||||
y: newY,
|
||||
width: domRect.width,
|
||||
@ -188,7 +186,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
|
||||
// Helper functions
|
||||
|
||||
function getResizeHandle(x: number, y: number, domRect: DOMRect): ResizeHandle {
|
||||
function getResizeHandle(x: number, y: number, domRect: Rectangle): ResizeHandle {
|
||||
const handleSize = 8;
|
||||
const tolerance = handleSize;
|
||||
|
||||
@ -211,17 +209,17 @@ function isNear(a: number, b: number, tolerance: number): boolean {
|
||||
return Math.abs(a - b) <= tolerance;
|
||||
}
|
||||
|
||||
function isPointInCropArea(x: number, y: number, domRect: DOMRect): boolean {
|
||||
function isPointInCropArea(x: number, y: number, domRect: Rectangle): boolean {
|
||||
return x >= domRect.x && x <= domRect.x + domRect.width &&
|
||||
y >= domRect.y && y <= domRect.y + domRect.height;
|
||||
}
|
||||
|
||||
function calculateResizedRect(
|
||||
handle: ResizeHandle,
|
||||
currentRect: DOMRect,
|
||||
currentRect: Rectangle,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
): DOMRect {
|
||||
): Rectangle {
|
||||
let { x, y, width, height } = currentRect;
|
||||
|
||||
switch (handle) {
|
||||
|
@ -5,10 +5,12 @@ import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
||||
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
||||
import CropAreaSelector from "./CropAreaSelector";
|
||||
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
|
||||
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
|
||||
import {
|
||||
calculatePDFBounds,
|
||||
PDFBounds,
|
||||
CropArea
|
||||
Rectangle
|
||||
} from "../../../utils/cropCoordinates";
|
||||
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
|
||||
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
|
||||
@ -69,12 +71,7 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
setPdfBounds(bounds);
|
||||
|
||||
// Initialize crop area to full PDF if parameters are still default
|
||||
const isDefault = parameters.parameters.width === 595 &&
|
||||
parameters.parameters.height === 842 &&
|
||||
parameters.parameters.x === 0 &&
|
||||
parameters.parameters.y === 0;
|
||||
|
||||
if (isDefault) {
|
||||
if (parameters.parameters.cropArea === DEFAULT_CROP_AREA) {
|
||||
parameters.resetToFullPDF(bounds);
|
||||
}
|
||||
|
||||
@ -83,10 +80,10 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
} catch (error) {
|
||||
console.error('Failed to load PDF dimensions:', error);
|
||||
// Fallback to A4 dimensions if PDF loading fails
|
||||
const bounds = calculatePDFBounds(595, 842, CONTAINER_SIZE, CONTAINER_SIZE);
|
||||
const bounds = calculatePDFBounds(PAGE_SIZES.A4.width, PAGE_SIZES.A4.height, CONTAINER_SIZE, CONTAINER_SIZE);
|
||||
setPdfBounds(bounds);
|
||||
|
||||
if (parameters.parameters.width === 595 && parameters.parameters.height === 842) {
|
||||
if (parameters.parameters.cropArea.width === PAGE_SIZES.A4.width && parameters.parameters.cropArea.height === PAGE_SIZES.A4.height) {
|
||||
parameters.resetToFullPDF(bounds);
|
||||
}
|
||||
}
|
||||
@ -100,14 +97,14 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
|
||||
|
||||
// Handle crop area changes from the selector
|
||||
const handleCropAreaChange = (newCropArea: CropArea) => {
|
||||
const handleCropAreaChange = (newCropArea: Rectangle) => {
|
||||
if (pdfBounds) {
|
||||
parameters.setCropArea(newCropArea, pdfBounds);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle manual coordinate input changes
|
||||
const handleCoordinateChange = (field: keyof CropArea, value: number | string) => {
|
||||
const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => {
|
||||
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(numValue)) return;
|
||||
|
||||
|
@ -1,37 +1,4 @@
|
||||
/**
|
||||
* Constants and configuration for the crop tool
|
||||
*/
|
||||
|
||||
// Default PDF page sizes in points (1 point = 1/72 inch)
|
||||
export const PAGE_SIZES = {
|
||||
A4: { width: 595, height: 842 },
|
||||
LETTER: { width: 612, height: 792 },
|
||||
A3: { width: 842, height: 1191 },
|
||||
A5: { width: 420, height: 595 },
|
||||
LEGAL: { width: 612, height: 1008 },
|
||||
} as const;
|
||||
|
||||
// Minimum crop area dimensions (in points)
|
||||
export const MIN_CROP_SIZE = {
|
||||
width: 10,
|
||||
height: 10,
|
||||
} as const;
|
||||
|
||||
// Maximum container size for thumbnail display
|
||||
export const CROP_CONTAINER_SIZE = 400;
|
||||
|
||||
// Crop overlay styling
|
||||
export const CROP_OVERLAY = {
|
||||
borderColor: '#ff4757',
|
||||
backgroundColor: 'rgba(255, 71, 87, 0.1)',
|
||||
borderWidth: 2,
|
||||
handleSize: 8,
|
||||
handleColor: '#ff4757',
|
||||
handleBorderColor: 'white',
|
||||
} as const;
|
||||
|
||||
// Coordinate precision (decimal places)
|
||||
export const COORDINATE_PRECISION = 1;
|
||||
import { PAGE_SIZES } from "./pageSizeConstants";
|
||||
|
||||
// Default crop area (covers entire page)
|
||||
export const DEFAULT_CROP_AREA = {
|
||||
@ -41,22 +8,5 @@ export const DEFAULT_CROP_AREA = {
|
||||
height: PAGE_SIZES.A4.height,
|
||||
} as const;
|
||||
|
||||
// Resize handle positions
|
||||
export const RESIZE_HANDLES = [
|
||||
'nw', 'ne', 'sw', 'se', // corners
|
||||
'n', 'e', 's', 'w' // edges
|
||||
] as const;
|
||||
|
||||
export type ResizeHandle = typeof RESIZE_HANDLES[number];
|
||||
|
||||
// Cursor styles for resize handles
|
||||
export const RESIZE_CURSORS: Record<ResizeHandle, string> = {
|
||||
'nw': 'nw-resize',
|
||||
'ne': 'ne-resize',
|
||||
'sw': 'sw-resize',
|
||||
'se': 'se-resize',
|
||||
'n': 'n-resize',
|
||||
'e': 'e-resize',
|
||||
's': 's-resize',
|
||||
'w': 'w-resize',
|
||||
} as const;
|
||||
export type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;
|
||||
|
8
frontend/src/constants/pageSizeConstants.ts
Normal file
8
frontend/src/constants/pageSizeConstants.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Default PDF page sizes in points (1 point = 1/72 inch)
|
||||
export const PAGE_SIZES = {
|
||||
A4: { width: 595, height: 842 },
|
||||
LETTER: { width: 612, height: 792 },
|
||||
A3: { width: 842, height: 1191 },
|
||||
A5: { width: 420, height: 595 },
|
||||
LEGAL: { width: 612, height: 1008 },
|
||||
} as const;
|
@ -7,12 +7,13 @@ import { CropParameters, defaultParameters } from './useCropParameters';
|
||||
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
const cropArea = parameters.cropArea;
|
||||
|
||||
// Backend expects precise float values for PDF coordinates
|
||||
formData.append("x", parameters.x.toString());
|
||||
formData.append("y", parameters.y.toString());
|
||||
formData.append("width", parameters.width.toString());
|
||||
formData.append("height", parameters.height.toString());
|
||||
formData.append("x", cropArea.x.toString());
|
||||
formData.append("y", cropArea.y.toString());
|
||||
formData.append("width", cropArea.width.toString());
|
||||
formData.append("height", cropArea.height.toString());
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
@ -1,31 +1,22 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
import { useCallback } from 'react';
|
||||
import { CropArea, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea } from '../../../utils/cropCoordinates';
|
||||
import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates';
|
||||
import { DEFAULT_CROP_AREA } from '../../../constants/cropConstants';
|
||||
|
||||
export interface CropParameters extends BaseParameters {
|
||||
/** X coordinate of crop area (PDF points, left edge) */
|
||||
x: number;
|
||||
/** Y coordinate of crop area (PDF points, bottom edge in PDF coordinate system) */
|
||||
y: number;
|
||||
/** Width of crop area (PDF points) */
|
||||
width: number;
|
||||
/** Height of crop area (PDF points) */
|
||||
height: number;
|
||||
cropArea: Rectangle;
|
||||
}
|
||||
|
||||
export const defaultParameters: CropParameters = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 595, // Default A4 width in points
|
||||
height: 842, // Default A4 height in points
|
||||
cropArea: DEFAULT_CROP_AREA,
|
||||
};
|
||||
|
||||
export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
||||
/** Set crop area with PDF bounds validation */
|
||||
setCropArea: (cropArea: CropArea, pdfBounds?: PDFBounds) => void;
|
||||
setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void;
|
||||
/** Get current crop area as CropArea object */
|
||||
getCropArea: () => CropArea;
|
||||
getCropArea: () => Rectangle;
|
||||
/** Reset to full PDF dimensions */
|
||||
resetToFullPDF: (pdfBounds: PDFBounds) => void;
|
||||
/** Check if current crop area is valid for the PDF */
|
||||
@ -33,7 +24,7 @@ export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
||||
/** Check if crop area covers the entire PDF */
|
||||
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
|
||||
/** Update crop area with constraints applied */
|
||||
updateCropAreaConstrained: (cropArea: Partial<CropArea>, pdfBounds?: PDFBounds) => void;
|
||||
updateCropAreaConstrained: (cropArea: Partial<Rectangle>, pdfBounds?: PDFBounds) => void;
|
||||
};
|
||||
|
||||
export const useCropParameters = (): CropParametersHook => {
|
||||
@ -41,37 +32,29 @@ export const useCropParameters = (): CropParametersHook => {
|
||||
defaultParameters,
|
||||
endpointName: 'crop',
|
||||
validateFn: (params) => {
|
||||
const rect = params.cropArea;
|
||||
// Basic validation - coordinates and dimensions must be positive
|
||||
return params.x >= 0 &&
|
||||
params.y >= 0 &&
|
||||
params.width > 0 &&
|
||||
params.height > 0;
|
||||
return rect.x >= 0 &&
|
||||
rect.y >= 0 &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0;
|
||||
},
|
||||
});
|
||||
|
||||
// Get current crop area as CropArea object
|
||||
const getCropArea = useCallback((): CropArea => {
|
||||
return {
|
||||
x: baseHook.parameters.x,
|
||||
y: baseHook.parameters.y,
|
||||
width: baseHook.parameters.width,
|
||||
height: baseHook.parameters.height,
|
||||
};
|
||||
const getCropArea = useCallback((): Rectangle => {
|
||||
return baseHook.parameters.cropArea;
|
||||
}, [baseHook.parameters]);
|
||||
|
||||
// Set crop area with optional PDF bounds validation
|
||||
const setCropArea = useCallback((cropArea: CropArea, pdfBounds?: PDFBounds) => {
|
||||
const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => {
|
||||
let finalCropArea = roundCropArea(cropArea);
|
||||
|
||||
// Apply PDF bounds constraints if provided
|
||||
if (pdfBounds) {
|
||||
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
|
||||
}
|
||||
|
||||
baseHook.updateParameter('x', finalCropArea.x);
|
||||
baseHook.updateParameter('y', finalCropArea.y);
|
||||
baseHook.updateParameter('width', finalCropArea.width);
|
||||
baseHook.updateParameter('height', finalCropArea.height);
|
||||
baseHook.updateParameter('cropArea', finalCropArea);
|
||||
}, [baseHook]);
|
||||
|
||||
// Reset to cover entire PDF
|
||||
@ -114,7 +97,7 @@ export const useCropParameters = (): CropParametersHook => {
|
||||
|
||||
// Update crop area with constraints applied
|
||||
const updateCropAreaConstrained = useCallback((
|
||||
partialCropArea: Partial<CropArea>,
|
||||
partialCropArea: Partial<Rectangle>,
|
||||
pdfBounds?: PDFBounds
|
||||
) => {
|
||||
const currentCropArea = getCropArea();
|
||||
@ -132,11 +115,12 @@ export const useCropParameters = (): CropParametersHook => {
|
||||
parameter: K,
|
||||
value: CropParameters[K]
|
||||
) => {
|
||||
// Ensure numeric parameters are positive
|
||||
if (typeof value === 'number' && parameter !== 'x' && parameter !== 'y') {
|
||||
value = Math.max(0.1, value) as CropParameters[K]; // Minimum 0.1 point
|
||||
} else if (typeof value === 'number') {
|
||||
value = Math.max(0, value) as CropParameters[K]; // x,y can be 0
|
||||
|
||||
if(isRectangle(value)) {
|
||||
value.x = Math.max(0.1, value.x); // Minimum 0.1 point
|
||||
value.x = Math.max(0.1, value.y); // Minimum 0.1 point
|
||||
value.width = Math.max(0, value.width); // Minimum 0 point
|
||||
value.height = Math.max(0, value.height); // Minimum 0 point
|
||||
}
|
||||
|
||||
baseHook.updateParameter(parameter, value);
|
||||
|
@ -8,26 +8,6 @@ import { FileId, BaseFileMetadata } from './file';
|
||||
// Re-export FileId for convenience
|
||||
export type { FileId };
|
||||
|
||||
export type ModeType =
|
||||
| 'viewer'
|
||||
| 'pageEditor'
|
||||
| 'fileEditor'
|
||||
| 'merge'
|
||||
| 'split'
|
||||
| 'compress'
|
||||
| 'ocr'
|
||||
| 'convert'
|
||||
| 'sanitize'
|
||||
| 'addPassword'
|
||||
| 'changePermissions'
|
||||
| 'addWatermark'
|
||||
| 'removePassword'
|
||||
| 'single-large-page'
|
||||
| 'repair'
|
||||
| 'unlockPdfForms'
|
||||
| 'removeCertificateSign'
|
||||
| 'auto-rename-pdf-file';
|
||||
|
||||
// Normalized state types
|
||||
export interface ProcessedFilePage {
|
||||
thumbnail?: string;
|
||||
@ -209,32 +189,6 @@ export function revokeFileResources(record: StirlingFileStub): void {
|
||||
}
|
||||
}
|
||||
|
||||
export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr' | 'sanitize';
|
||||
|
||||
export interface FileOperation {
|
||||
id: string;
|
||||
type: OperationType;
|
||||
timestamp: number;
|
||||
fileIds: FileId[];
|
||||
status: 'pending' | 'applied' | 'failed';
|
||||
data?: any;
|
||||
metadata?: {
|
||||
originalFileName?: string;
|
||||
outputFileNames?: string[];
|
||||
fileSize?: number;
|
||||
pageCount?: number;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileOperationHistory {
|
||||
fileId: FileId;
|
||||
fileName: string;
|
||||
operations: (FileOperation | PageOperation)[];
|
||||
createdAt: number;
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
export interface ViewerConfig {
|
||||
zoom: number;
|
||||
currentPage: number;
|
||||
|
@ -9,4 +9,4 @@ export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor';
|
||||
// Type guard using the same source of truth
|
||||
export const isValidWorkbench = (value: string): value is WorkbenchType => {
|
||||
return WORKBENCH_TYPES.includes(value as WorkbenchType);
|
||||
};
|
||||
};
|
||||
|
@ -19,26 +19,33 @@ export interface PDFBounds {
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export interface CropArea {
|
||||
/** X coordinate in PDF points (0 = left edge) */
|
||||
export interface Rectangle {
|
||||
/** X coordinate */
|
||||
x: number;
|
||||
/** Y coordinate in PDF points (0 = bottom edge, PDF coordinate system) */
|
||||
/** Y coordinate */
|
||||
y: number;
|
||||
/** Width in PDF points */
|
||||
/** Width */
|
||||
width: number;
|
||||
/** Height in PDF points */
|
||||
/** Height */
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface DOMRect {
|
||||
/** X coordinate in DOM pixels relative to thumbnail container */
|
||||
x: number;
|
||||
/** Y coordinate in DOM pixels relative to thumbnail container */
|
||||
y: number;
|
||||
/** Width in DOM pixels */
|
||||
width: number;
|
||||
/** Height in DOM pixels */
|
||||
height: number;
|
||||
/** Runtime type guard */
|
||||
export function isRectangle(value: unknown): value is Rectangle {
|
||||
if (value === null || typeof value !== "object") return false;
|
||||
|
||||
const r = value as Record<string, unknown>;
|
||||
const isNum = (n: unknown): n is number =>
|
||||
typeof n === "number" && Number.isFinite(n);
|
||||
|
||||
return (
|
||||
isNum(r.x) &&
|
||||
isNum(r.y) &&
|
||||
isNum(r.width) &&
|
||||
isNum(r.height) &&
|
||||
r.width >= 0 &&
|
||||
r.height >= 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,9 +86,9 @@ export const calculatePDFBounds = (
|
||||
* Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin)
|
||||
*/
|
||||
export const domToPDFCoordinates = (
|
||||
domRect: DOMRect,
|
||||
domRect: Rectangle,
|
||||
pdfBounds: PDFBounds
|
||||
): CropArea => {
|
||||
): Rectangle => {
|
||||
// Convert DOM coordinates to thumbnail-relative coordinates
|
||||
const thumbX = domRect.x - pdfBounds.offsetX;
|
||||
const thumbY = domRect.y - pdfBounds.offsetY;
|
||||
@ -104,9 +111,9 @@ export const domToPDFCoordinates = (
|
||||
* Convert PDF coordinates to DOM coordinates (relative to container)
|
||||
*/
|
||||
export const pdfToDOMCoordinates = (
|
||||
cropArea: CropArea,
|
||||
cropArea: Rectangle,
|
||||
pdfBounds: PDFBounds
|
||||
): DOMRect => {
|
||||
): Rectangle => {
|
||||
// Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis)
|
||||
const thumbX = cropArea.x * pdfBounds.scale;
|
||||
const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale;
|
||||
@ -126,9 +133,9 @@ export const pdfToDOMCoordinates = (
|
||||
* Constrain a crop area to stay within PDF bounds
|
||||
*/
|
||||
export const constrainCropAreaToPDF = (
|
||||
cropArea: CropArea,
|
||||
cropArea: Rectangle,
|
||||
pdfBounds: PDFBounds
|
||||
): CropArea => {
|
||||
): Rectangle => {
|
||||
// Ensure crop area doesn't extend beyond PDF boundaries
|
||||
const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width);
|
||||
const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height);
|
||||
@ -145,9 +152,9 @@ export const constrainCropAreaToPDF = (
|
||||
* Constrain DOM coordinates to stay within thumbnail bounds
|
||||
*/
|
||||
export const constrainDOMRectToThumbnail = (
|
||||
domRect: DOMRect,
|
||||
domRect: Rectangle,
|
||||
pdfBounds: PDFBounds
|
||||
): DOMRect => {
|
||||
): Rectangle => {
|
||||
const thumbnailLeft = pdfBounds.offsetX;
|
||||
const thumbnailTop = pdfBounds.offsetY;
|
||||
const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth;
|
||||
@ -189,7 +196,7 @@ export const isPointInThumbnail = (
|
||||
/**
|
||||
* Create a default crop area that covers the entire PDF
|
||||
*/
|
||||
export const createFullPDFCropArea = (pdfBounds: PDFBounds): CropArea => {
|
||||
export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -201,7 +208,7 @@ export const createFullPDFCropArea = (pdfBounds: PDFBounds): CropArea => {
|
||||
/**
|
||||
* Round crop coordinates to reasonable precision (0.1 point)
|
||||
*/
|
||||
export const roundCropArea = (cropArea: CropArea): CropArea => {
|
||||
export const roundCropArea = (cropArea: Rectangle): Rectangle => {
|
||||
return {
|
||||
x: Math.round(cropArea.x * 10) / 10,
|
||||
y: Math.round(cropArea.y * 10) / 10,
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { FileId } from '../types/file';
|
||||
import { FileOperation } from '../types/fileContext';
|
||||
|
||||
/**
|
||||
* Creates operation tracking data for FileContext integration
|
||||
*/
|
||||
export const createOperation = <TParams = void>(
|
||||
operationType: string,
|
||||
_params: TParams,
|
||||
selectedFiles: File[]
|
||||
): { operation: FileOperation; operationId: string; fileId: FileId } => {
|
||||
const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const fileId = selectedFiles.map(f => f.name).join(',') as FileId;
|
||||
|
||||
const operation: FileOperation = {
|
||||
id: operationId,
|
||||
type: operationType,
|
||||
timestamp: Date.now(),
|
||||
fileIds: selectedFiles.map(f => f.name),
|
||||
status: 'pending',
|
||||
metadata: {
|
||||
originalFileName: selectedFiles[0]?.name,
|
||||
fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0)
|
||||
}
|
||||
} as any /* FIX ME*/;
|
||||
|
||||
return { operation, operationId, fileId };
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user