mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-23 20:16:15 +00:00
Compare commits
No commits in common. "33f8172d5dd41baae1200b13c1efaf900a47065a" and "1fa6b478bc988203b5d646c46def182cbd78a0b2" have entirely different histories.
33f8172d5d
...
1fa6b478bc
@ -19,8 +19,8 @@ const viewOptionStyle = {
|
|||||||
|
|
||||||
|
|
||||||
// Build view options showing text always
|
// Build view options showing text always
|
||||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
|
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [
|
||||||
const viewerOption = {
|
{
|
||||||
label: (
|
label: (
|
||||||
<div style={viewOptionStyle as React.CSSProperties}>
|
<div style={viewOptionStyle as React.CSSProperties}>
|
||||||
{switchingTo === "viewer" ? (
|
{switchingTo === "viewer" ? (
|
||||||
@ -32,9 +32,8 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: "viewer",
|
value: "viewer",
|
||||||
};
|
},
|
||||||
|
{
|
||||||
const pageEditorOption = {
|
|
||||||
label: (
|
label: (
|
||||||
<div style={viewOptionStyle as React.CSSProperties}>
|
<div style={viewOptionStyle as React.CSSProperties}>
|
||||||
{currentView === "pageEditor" ? (
|
{currentView === "pageEditor" ? (
|
||||||
@ -51,9 +50,8 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: "pageEditor",
|
value: "pageEditor",
|
||||||
};
|
},
|
||||||
|
{
|
||||||
const fileEditorOption = {
|
|
||||||
label: (
|
label: (
|
||||||
<div style={viewOptionStyle as React.CSSProperties}>
|
<div style={viewOptionStyle as React.CSSProperties}>
|
||||||
{currentView === "fileEditor" ? (
|
{currentView === "fileEditor" ? (
|
||||||
@ -70,15 +68,8 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
value: "fileEditor",
|
value: "fileEditor",
|
||||||
};
|
},
|
||||||
|
|
||||||
// Build options array conditionally
|
|
||||||
return [
|
|
||||||
viewerOption,
|
|
||||||
...(isToolSelected ? [] : [pageEditorOption]),
|
|
||||||
fileEditorOption,
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
interface TopControlsProps {
|
interface TopControlsProps {
|
||||||
currentView: WorkbenchType;
|
currentView: WorkbenchType;
|
||||||
@ -120,9 +111,10 @@ const TopControls = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||||
|
{!isToolSelected && (
|
||||||
<div className="flex justify-center mt-[0.5rem]">
|
<div className="flex justify-center mt-[0.5rem]">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={createViewOptions(currentView, switchingTo, isToolSelected)}
|
data={createViewOptions(currentView, switchingTo)}
|
||||||
value={currentView}
|
value={currentView}
|
||||||
onChange={handleViewChange}
|
onChange={handleViewChange}
|
||||||
color="blue"
|
color="blue"
|
||||||
@ -151,6 +143,7 @@ const TopControls = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,27 +2,29 @@ import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|||||||
import { Box, useMantineTheme, MantineTheme } from '@mantine/core';
|
import { Box, useMantineTheme, MantineTheme } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
PDFBounds,
|
PDFBounds,
|
||||||
Rectangle,
|
CropArea,
|
||||||
|
DOMRect,
|
||||||
domToPDFCoordinates,
|
domToPDFCoordinates,
|
||||||
pdfToDOMCoordinates,
|
pdfToDOMCoordinates,
|
||||||
constrainDOMRectToThumbnail,
|
constrainDOMRectToThumbnail,
|
||||||
isPointInThumbnail
|
isPointInThumbnail
|
||||||
} from '../../../utils/cropCoordinates';
|
} from '../../../utils/cropCoordinates';
|
||||||
import { type ResizeHandle } from '../../../constants/cropConstants';
|
|
||||||
|
|
||||||
interface CropAreaSelectorProps {
|
interface CropAreaSelectorProps {
|
||||||
/** PDF bounds for coordinate conversion */
|
/** PDF bounds for coordinate conversion */
|
||||||
pdfBounds: PDFBounds;
|
pdfBounds: PDFBounds;
|
||||||
/** Current crop area in PDF coordinates */
|
/** Current crop area in PDF coordinates */
|
||||||
cropArea: Rectangle;
|
cropArea: CropArea;
|
||||||
/** Callback when crop area changes */
|
/** Callback when crop area changes */
|
||||||
onCropAreaChange: (cropArea: Rectangle) => void;
|
onCropAreaChange: (cropArea: CropArea) => void;
|
||||||
/** Whether the selector is disabled */
|
/** Whether the selector is disabled */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** Child content (typically the PDF thumbnail) */
|
/** Child content (typically the PDF thumbnail) */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;
|
||||||
|
|
||||||
const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||||
pdfBounds,
|
pdfBounds,
|
||||||
cropArea,
|
cropArea,
|
||||||
@ -37,7 +39,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isResizing, setIsResizing] = useState<ResizeHandle>(null);
|
const [isResizing, setIsResizing] = useState<ResizeHandle>(null);
|
||||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||||
const [initialCropArea, setInitialCropArea] = useState<Rectangle>(cropArea);
|
const [initialCropArea, setInitialCropArea] = useState<CropArea>(cropArea);
|
||||||
|
|
||||||
// Convert PDF crop area to DOM coordinates for display
|
// Convert PDF crop area to DOM coordinates for display
|
||||||
const domRect = pdfToDOMCoordinates(cropArea, pdfBounds);
|
const domRect = pdfToDOMCoordinates(cropArea, pdfBounds);
|
||||||
@ -83,7 +85,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Start new crop selection
|
// Start new crop selection
|
||||||
const newDomRect: Rectangle = { x, y, width: 20, height: 20 };
|
const newDomRect: DOMRect = { x, y, width: 20, height: 20 };
|
||||||
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
||||||
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
||||||
|
|
||||||
@ -105,7 +107,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
|||||||
const newX = x - dragStart.x;
|
const newX = x - dragStart.x;
|
||||||
const newY = y - dragStart.y;
|
const newY = y - dragStart.y;
|
||||||
|
|
||||||
const newDomRect: Rectangle = {
|
const newDomRect: DOMRect = {
|
||||||
x: newX,
|
x: newX,
|
||||||
y: newY,
|
y: newY,
|
||||||
width: domRect.width,
|
width: domRect.width,
|
||||||
@ -186,7 +188,7 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
|||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
||||||
function getResizeHandle(x: number, y: number, domRect: Rectangle): ResizeHandle {
|
function getResizeHandle(x: number, y: number, domRect: DOMRect): ResizeHandle {
|
||||||
const handleSize = 8;
|
const handleSize = 8;
|
||||||
const tolerance = handleSize;
|
const tolerance = handleSize;
|
||||||
|
|
||||||
@ -209,17 +211,17 @@ function isNear(a: number, b: number, tolerance: number): boolean {
|
|||||||
return Math.abs(a - b) <= tolerance;
|
return Math.abs(a - b) <= tolerance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPointInCropArea(x: number, y: number, domRect: Rectangle): boolean {
|
function isPointInCropArea(x: number, y: number, domRect: DOMRect): boolean {
|
||||||
return x >= domRect.x && x <= domRect.x + domRect.width &&
|
return x >= domRect.x && x <= domRect.x + domRect.width &&
|
||||||
y >= domRect.y && y <= domRect.y + domRect.height;
|
y >= domRect.y && y <= domRect.y + domRect.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateResizedRect(
|
function calculateResizedRect(
|
||||||
handle: ResizeHandle,
|
handle: ResizeHandle,
|
||||||
currentRect: Rectangle,
|
currentRect: DOMRect,
|
||||||
mouseX: number,
|
mouseX: number,
|
||||||
mouseY: number,
|
mouseY: number,
|
||||||
): Rectangle {
|
): DOMRect {
|
||||||
let { x, y, width, height } = currentRect;
|
let { x, y, width, height } = currentRect;
|
||||||
|
|
||||||
switch (handle) {
|
switch (handle) {
|
||||||
|
@ -5,12 +5,10 @@ import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|||||||
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
||||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
||||||
import CropAreaSelector from "./CropAreaSelector";
|
import CropAreaSelector from "./CropAreaSelector";
|
||||||
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
|
|
||||||
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
|
|
||||||
import {
|
import {
|
||||||
calculatePDFBounds,
|
calculatePDFBounds,
|
||||||
PDFBounds,
|
PDFBounds,
|
||||||
Rectangle
|
CropArea
|
||||||
} from "../../../utils/cropCoordinates";
|
} from "../../../utils/cropCoordinates";
|
||||||
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
|
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
|
||||||
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
|
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
|
||||||
@ -71,7 +69,12 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|||||||
setPdfBounds(bounds);
|
setPdfBounds(bounds);
|
||||||
|
|
||||||
// Initialize crop area to full PDF if parameters are still default
|
// Initialize crop area to full PDF if parameters are still default
|
||||||
if (parameters.parameters.cropArea === DEFAULT_CROP_AREA) {
|
const isDefault = parameters.parameters.width === 595 &&
|
||||||
|
parameters.parameters.height === 842 &&
|
||||||
|
parameters.parameters.x === 0 &&
|
||||||
|
parameters.parameters.y === 0;
|
||||||
|
|
||||||
|
if (isDefault) {
|
||||||
parameters.resetToFullPDF(bounds);
|
parameters.resetToFullPDF(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,10 +83,10 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load PDF dimensions:', error);
|
console.error('Failed to load PDF dimensions:', error);
|
||||||
// Fallback to A4 dimensions if PDF loading fails
|
// Fallback to A4 dimensions if PDF loading fails
|
||||||
const bounds = calculatePDFBounds(PAGE_SIZES.A4.width, PAGE_SIZES.A4.height, CONTAINER_SIZE, CONTAINER_SIZE);
|
const bounds = calculatePDFBounds(595, 842, CONTAINER_SIZE, CONTAINER_SIZE);
|
||||||
setPdfBounds(bounds);
|
setPdfBounds(bounds);
|
||||||
|
|
||||||
if (parameters.parameters.cropArea.width === PAGE_SIZES.A4.width && parameters.parameters.cropArea.height === PAGE_SIZES.A4.height) {
|
if (parameters.parameters.width === 595 && parameters.parameters.height === 842) {
|
||||||
parameters.resetToFullPDF(bounds);
|
parameters.resetToFullPDF(bounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,14 +100,14 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|||||||
|
|
||||||
|
|
||||||
// Handle crop area changes from the selector
|
// Handle crop area changes from the selector
|
||||||
const handleCropAreaChange = (newCropArea: Rectangle) => {
|
const handleCropAreaChange = (newCropArea: CropArea) => {
|
||||||
if (pdfBounds) {
|
if (pdfBounds) {
|
||||||
parameters.setCropArea(newCropArea, pdfBounds);
|
parameters.setCropArea(newCropArea, pdfBounds);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle manual coordinate input changes
|
// Handle manual coordinate input changes
|
||||||
const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => {
|
const handleCoordinateChange = (field: keyof CropArea, value: number | string) => {
|
||||||
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||||
if (isNaN(numValue)) return;
|
if (isNaN(numValue)) return;
|
||||||
|
|
||||||
|
@ -1,4 +1,37 @@
|
|||||||
import { PAGE_SIZES } from "./pageSizeConstants";
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
// Default crop area (covers entire page)
|
// Default crop area (covers entire page)
|
||||||
export const DEFAULT_CROP_AREA = {
|
export const DEFAULT_CROP_AREA = {
|
||||||
@ -8,5 +41,22 @@ export const DEFAULT_CROP_AREA = {
|
|||||||
height: PAGE_SIZES.A4.height,
|
height: PAGE_SIZES.A4.height,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Resize handle positions
|
||||||
|
export const RESIZE_HANDLES = [
|
||||||
|
'nw', 'ne', 'sw', 'se', // corners
|
||||||
|
'n', 'e', 's', 'w' // edges
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;
|
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;
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
// 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,13 +7,12 @@ import { CropParameters, defaultParameters } from './useCropParameters';
|
|||||||
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
|
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
const cropArea = parameters.cropArea;
|
|
||||||
|
|
||||||
// Backend expects precise float values for PDF coordinates
|
// Backend expects precise float values for PDF coordinates
|
||||||
formData.append("x", cropArea.x.toString());
|
formData.append("x", parameters.x.toString());
|
||||||
formData.append("y", cropArea.y.toString());
|
formData.append("y", parameters.y.toString());
|
||||||
formData.append("width", cropArea.width.toString());
|
formData.append("width", parameters.width.toString());
|
||||||
formData.append("height", cropArea.height.toString());
|
formData.append("height", parameters.height.toString());
|
||||||
|
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
import { BaseParameters } from '../../../types/parameters';
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates';
|
import { CropArea, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea } from '../../../utils/cropCoordinates';
|
||||||
import { DEFAULT_CROP_AREA } from '../../../constants/cropConstants';
|
|
||||||
|
|
||||||
export interface CropParameters extends BaseParameters {
|
export interface CropParameters extends BaseParameters {
|
||||||
cropArea: Rectangle;
|
/** 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultParameters: CropParameters = {
|
export const defaultParameters: CropParameters = {
|
||||||
cropArea: DEFAULT_CROP_AREA,
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 595, // Default A4 width in points
|
||||||
|
height: 842, // Default A4 height in points
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
||||||
/** Set crop area with PDF bounds validation */
|
/** Set crop area with PDF bounds validation */
|
||||||
setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void;
|
setCropArea: (cropArea: CropArea, pdfBounds?: PDFBounds) => void;
|
||||||
/** Get current crop area as CropArea object */
|
/** Get current crop area as CropArea object */
|
||||||
getCropArea: () => Rectangle;
|
getCropArea: () => CropArea;
|
||||||
/** Reset to full PDF dimensions */
|
/** Reset to full PDF dimensions */
|
||||||
resetToFullPDF: (pdfBounds: PDFBounds) => void;
|
resetToFullPDF: (pdfBounds: PDFBounds) => void;
|
||||||
/** Check if current crop area is valid for the PDF */
|
/** Check if current crop area is valid for the PDF */
|
||||||
@ -24,7 +33,7 @@ export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
|||||||
/** Check if crop area covers the entire PDF */
|
/** Check if crop area covers the entire PDF */
|
||||||
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
|
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
|
||||||
/** Update crop area with constraints applied */
|
/** Update crop area with constraints applied */
|
||||||
updateCropAreaConstrained: (cropArea: Partial<Rectangle>, pdfBounds?: PDFBounds) => void;
|
updateCropAreaConstrained: (cropArea: Partial<CropArea>, pdfBounds?: PDFBounds) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCropParameters = (): CropParametersHook => {
|
export const useCropParameters = (): CropParametersHook => {
|
||||||
@ -32,29 +41,37 @@ export const useCropParameters = (): CropParametersHook => {
|
|||||||
defaultParameters,
|
defaultParameters,
|
||||||
endpointName: 'crop',
|
endpointName: 'crop',
|
||||||
validateFn: (params) => {
|
validateFn: (params) => {
|
||||||
const rect = params.cropArea;
|
|
||||||
// Basic validation - coordinates and dimensions must be positive
|
// Basic validation - coordinates and dimensions must be positive
|
||||||
return rect.x >= 0 &&
|
return params.x >= 0 &&
|
||||||
rect.y >= 0 &&
|
params.y >= 0 &&
|
||||||
rect.width > 0 &&
|
params.width > 0 &&
|
||||||
rect.height > 0;
|
params.height > 0;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get current crop area as CropArea object
|
// Get current crop area as CropArea object
|
||||||
const getCropArea = useCallback((): Rectangle => {
|
const getCropArea = useCallback((): CropArea => {
|
||||||
return baseHook.parameters.cropArea;
|
return {
|
||||||
|
x: baseHook.parameters.x,
|
||||||
|
y: baseHook.parameters.y,
|
||||||
|
width: baseHook.parameters.width,
|
||||||
|
height: baseHook.parameters.height,
|
||||||
|
};
|
||||||
}, [baseHook.parameters]);
|
}, [baseHook.parameters]);
|
||||||
|
|
||||||
// Set crop area with optional PDF bounds validation
|
// Set crop area with optional PDF bounds validation
|
||||||
const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => {
|
const setCropArea = useCallback((cropArea: CropArea, pdfBounds?: PDFBounds) => {
|
||||||
let finalCropArea = roundCropArea(cropArea);
|
let finalCropArea = roundCropArea(cropArea);
|
||||||
|
|
||||||
// Apply PDF bounds constraints if provided
|
// Apply PDF bounds constraints if provided
|
||||||
if (pdfBounds) {
|
if (pdfBounds) {
|
||||||
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
|
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
|
||||||
}
|
}
|
||||||
baseHook.updateParameter('cropArea', finalCropArea);
|
|
||||||
|
baseHook.updateParameter('x', finalCropArea.x);
|
||||||
|
baseHook.updateParameter('y', finalCropArea.y);
|
||||||
|
baseHook.updateParameter('width', finalCropArea.width);
|
||||||
|
baseHook.updateParameter('height', finalCropArea.height);
|
||||||
}, [baseHook]);
|
}, [baseHook]);
|
||||||
|
|
||||||
// Reset to cover entire PDF
|
// Reset to cover entire PDF
|
||||||
@ -97,7 +114,7 @@ export const useCropParameters = (): CropParametersHook => {
|
|||||||
|
|
||||||
// Update crop area with constraints applied
|
// Update crop area with constraints applied
|
||||||
const updateCropAreaConstrained = useCallback((
|
const updateCropAreaConstrained = useCallback((
|
||||||
partialCropArea: Partial<Rectangle>,
|
partialCropArea: Partial<CropArea>,
|
||||||
pdfBounds?: PDFBounds
|
pdfBounds?: PDFBounds
|
||||||
) => {
|
) => {
|
||||||
const currentCropArea = getCropArea();
|
const currentCropArea = getCropArea();
|
||||||
@ -115,12 +132,11 @@ export const useCropParameters = (): CropParametersHook => {
|
|||||||
parameter: K,
|
parameter: K,
|
||||||
value: CropParameters[K]
|
value: CropParameters[K]
|
||||||
) => {
|
) => {
|
||||||
|
// Ensure numeric parameters are positive
|
||||||
if(isRectangle(value)) {
|
if (typeof value === 'number' && parameter !== 'x' && parameter !== 'y') {
|
||||||
value.x = Math.max(0.1, value.x); // Minimum 0.1 point
|
value = Math.max(0.1, value) as CropParameters[K]; // Minimum 0.1 point
|
||||||
value.x = Math.max(0.1, value.y); // Minimum 0.1 point
|
} else if (typeof value === 'number') {
|
||||||
value.width = Math.max(0, value.width); // Minimum 0 point
|
value = Math.max(0, value) as CropParameters[K]; // x,y can be 0
|
||||||
value.height = Math.max(0, value.height); // Minimum 0 point
|
|
||||||
}
|
}
|
||||||
|
|
||||||
baseHook.updateParameter(parameter, value);
|
baseHook.updateParameter(parameter, value);
|
||||||
|
@ -8,6 +8,26 @@ import { FileId, BaseFileMetadata } from './file';
|
|||||||
// Re-export FileId for convenience
|
// Re-export FileId for convenience
|
||||||
export type { FileId };
|
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
|
// Normalized state types
|
||||||
export interface ProcessedFilePage {
|
export interface ProcessedFilePage {
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
@ -189,6 +209,32 @@ 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 {
|
export interface ViewerConfig {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
|
@ -19,33 +19,26 @@ export interface PDFBounds {
|
|||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Rectangle {
|
export interface CropArea {
|
||||||
/** X coordinate */
|
/** X coordinate in PDF points (0 = left edge) */
|
||||||
x: number;
|
x: number;
|
||||||
/** Y coordinate */
|
/** Y coordinate in PDF points (0 = bottom edge, PDF coordinate system) */
|
||||||
y: number;
|
y: number;
|
||||||
/** Width */
|
/** Width in PDF points */
|
||||||
width: number;
|
width: number;
|
||||||
/** Height */
|
/** Height in PDF points */
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Runtime type guard */
|
export interface DOMRect {
|
||||||
export function isRectangle(value: unknown): value is Rectangle {
|
/** X coordinate in DOM pixels relative to thumbnail container */
|
||||||
if (value === null || typeof value !== "object") return false;
|
x: number;
|
||||||
|
/** Y coordinate in DOM pixels relative to thumbnail container */
|
||||||
const r = value as Record<string, unknown>;
|
y: number;
|
||||||
const isNum = (n: unknown): n is number =>
|
/** Width in DOM pixels */
|
||||||
typeof n === "number" && Number.isFinite(n);
|
width: number;
|
||||||
|
/** Height in DOM pixels */
|
||||||
return (
|
height: number;
|
||||||
isNum(r.x) &&
|
|
||||||
isNum(r.y) &&
|
|
||||||
isNum(r.width) &&
|
|
||||||
isNum(r.height) &&
|
|
||||||
r.width >= 0 &&
|
|
||||||
r.height >= 0
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,9 +79,9 @@ export const calculatePDFBounds = (
|
|||||||
* Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin)
|
* Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin)
|
||||||
*/
|
*/
|
||||||
export const domToPDFCoordinates = (
|
export const domToPDFCoordinates = (
|
||||||
domRect: Rectangle,
|
domRect: DOMRect,
|
||||||
pdfBounds: PDFBounds
|
pdfBounds: PDFBounds
|
||||||
): Rectangle => {
|
): CropArea => {
|
||||||
// Convert DOM coordinates to thumbnail-relative coordinates
|
// Convert DOM coordinates to thumbnail-relative coordinates
|
||||||
const thumbX = domRect.x - pdfBounds.offsetX;
|
const thumbX = domRect.x - pdfBounds.offsetX;
|
||||||
const thumbY = domRect.y - pdfBounds.offsetY;
|
const thumbY = domRect.y - pdfBounds.offsetY;
|
||||||
@ -111,9 +104,9 @@ export const domToPDFCoordinates = (
|
|||||||
* Convert PDF coordinates to DOM coordinates (relative to container)
|
* Convert PDF coordinates to DOM coordinates (relative to container)
|
||||||
*/
|
*/
|
||||||
export const pdfToDOMCoordinates = (
|
export const pdfToDOMCoordinates = (
|
||||||
cropArea: Rectangle,
|
cropArea: CropArea,
|
||||||
pdfBounds: PDFBounds
|
pdfBounds: PDFBounds
|
||||||
): Rectangle => {
|
): DOMRect => {
|
||||||
// Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis)
|
// Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis)
|
||||||
const thumbX = cropArea.x * pdfBounds.scale;
|
const thumbX = cropArea.x * pdfBounds.scale;
|
||||||
const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale;
|
const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale;
|
||||||
@ -133,9 +126,9 @@ export const pdfToDOMCoordinates = (
|
|||||||
* Constrain a crop area to stay within PDF bounds
|
* Constrain a crop area to stay within PDF bounds
|
||||||
*/
|
*/
|
||||||
export const constrainCropAreaToPDF = (
|
export const constrainCropAreaToPDF = (
|
||||||
cropArea: Rectangle,
|
cropArea: CropArea,
|
||||||
pdfBounds: PDFBounds
|
pdfBounds: PDFBounds
|
||||||
): Rectangle => {
|
): CropArea => {
|
||||||
// Ensure crop area doesn't extend beyond PDF boundaries
|
// Ensure crop area doesn't extend beyond PDF boundaries
|
||||||
const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width);
|
const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width);
|
||||||
const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height);
|
const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height);
|
||||||
@ -152,9 +145,9 @@ export const constrainCropAreaToPDF = (
|
|||||||
* Constrain DOM coordinates to stay within thumbnail bounds
|
* Constrain DOM coordinates to stay within thumbnail bounds
|
||||||
*/
|
*/
|
||||||
export const constrainDOMRectToThumbnail = (
|
export const constrainDOMRectToThumbnail = (
|
||||||
domRect: Rectangle,
|
domRect: DOMRect,
|
||||||
pdfBounds: PDFBounds
|
pdfBounds: PDFBounds
|
||||||
): Rectangle => {
|
): DOMRect => {
|
||||||
const thumbnailLeft = pdfBounds.offsetX;
|
const thumbnailLeft = pdfBounds.offsetX;
|
||||||
const thumbnailTop = pdfBounds.offsetY;
|
const thumbnailTop = pdfBounds.offsetY;
|
||||||
const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth;
|
const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth;
|
||||||
@ -196,7 +189,7 @@ export const isPointInThumbnail = (
|
|||||||
/**
|
/**
|
||||||
* Create a default crop area that covers the entire PDF
|
* Create a default crop area that covers the entire PDF
|
||||||
*/
|
*/
|
||||||
export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => {
|
export const createFullPDFCropArea = (pdfBounds: PDFBounds): CropArea => {
|
||||||
return {
|
return {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@ -208,7 +201,7 @@ export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => {
|
|||||||
/**
|
/**
|
||||||
* Round crop coordinates to reasonable precision (0.1 point)
|
* Round crop coordinates to reasonable precision (0.1 point)
|
||||||
*/
|
*/
|
||||||
export const roundCropArea = (cropArea: Rectangle): Rectangle => {
|
export const roundCropArea = (cropArea: CropArea): CropArea => {
|
||||||
return {
|
return {
|
||||||
x: Math.round(cropArea.x * 10) / 10,
|
x: Math.round(cropArea.x * 10) / 10,
|
||||||
y: Math.round(cropArea.y * 10) / 10,
|
y: Math.round(cropArea.y * 10) / 10,
|
||||||
|
28
frontend/src/utils/toolOperationTracker.ts
Normal file
28
frontend/src/utils/toolOperationTracker.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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