mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-20 13:47:46 +02:00
Add Remove Password UI into V2 (#4214)
# Description of Changes - Add UI for Remove Password tool - Fix more translation warnings that were being thrown in the console - Add an encrypted PDF thumbnail and refactor thumbnail generation code
This commit is contained in:
parent
4c17c520d7
commit
acbebd67a3
@ -35,11 +35,20 @@
|
|||||||
"true": "True",
|
"true": "True",
|
||||||
"false": "False",
|
"false": "False",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
|
"app": {
|
||||||
|
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||||
|
},
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveToBrowser": "Save to Browser",
|
"saveToBrowser": "Save to Browser",
|
||||||
|
"download": "Download",
|
||||||
|
"editYourNewFiles": "Edit your new file(s)",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"fileSelected": "Selected: {{filename}}",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
|
"files": {
|
||||||
|
"title": "Files",
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
"noFavourites": "No favourites added",
|
"noFavourites": "No favourites added",
|
||||||
"downloadComplete": "Download Complete",
|
"downloadComplete": "Download Complete",
|
||||||
"bored": "Bored Waiting?",
|
"bored": "Bored Waiting?",
|
||||||
@ -119,6 +128,7 @@
|
|||||||
"page": "Page",
|
"page": "Page",
|
||||||
"pages": "Pages",
|
"pages": "Pages",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
|
"review": "Review",
|
||||||
"addToDoc": "Add to Document",
|
"addToDoc": "Add to Document",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
@ -811,16 +821,6 @@
|
|||||||
"removePages": {
|
"removePages": {
|
||||||
"tags": "Remove pages,delete pages"
|
"tags": "Remove pages,delete pages"
|
||||||
},
|
},
|
||||||
"removePassword": {
|
|
||||||
"tags": "secure,Decrypt,security,unpassword,delete password",
|
|
||||||
"title": "Remove password",
|
|
||||||
"header": "Remove password (Decrypt)",
|
|
||||||
"selectText": {
|
|
||||||
"1": "Select PDF to Decrypt",
|
|
||||||
"2": "Password"
|
|
||||||
},
|
|
||||||
"submit": "Remove"
|
|
||||||
},
|
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
"tags": "squish,small,tiny"
|
"tags": "squish,small,tiny"
|
||||||
},
|
},
|
||||||
@ -863,6 +863,7 @@
|
|||||||
"ocr": {
|
"ocr": {
|
||||||
"tags": "recognition,text,image,scan,read,identify,detection,editable",
|
"tags": "recognition,text,image,scan,read,identify,detection,editable",
|
||||||
"title": "OCR / Scan Cleanup",
|
"title": "OCR / Scan Cleanup",
|
||||||
|
"desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text.",
|
||||||
"header": "Cleanup Scans / OCR (Optical Character Recognition)",
|
"header": "Cleanup Scans / OCR (Optical Character Recognition)",
|
||||||
"selectText": {
|
"selectText": {
|
||||||
"1": "Select languages that are to be detected within the PDF (Ones listed are the ones currently detected):",
|
"1": "Select languages that are to be detected within the PDF (Ones listed are the ones currently detected):",
|
||||||
@ -1423,6 +1424,7 @@
|
|||||||
},
|
},
|
||||||
"compress": {
|
"compress": {
|
||||||
"title": "Compress",
|
"title": "Compress",
|
||||||
|
"desc": "Compress PDFs to reduce their file size.",
|
||||||
"header": "Compress PDF",
|
"header": "Compress PDF",
|
||||||
"credit": "This service uses qpdf for PDF Compress/Optimisation.",
|
"credit": "This service uses qpdf for PDF Compress/Optimisation.",
|
||||||
"grayscale": {
|
"grayscale": {
|
||||||
@ -1781,8 +1783,8 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"failed": "An error occurred while encrypting the PDF."
|
"failed": "An error occurred while encrypting the PDF."
|
||||||
},
|
},
|
||||||
"title": "Passwords & Encryption",
|
|
||||||
"passwords": {
|
"passwords": {
|
||||||
|
"stepTitle": "Passwords & Encryption",
|
||||||
"completed": "Passwords configured",
|
"completed": "Passwords configured",
|
||||||
"user": {
|
"user": {
|
||||||
"label": "User Password",
|
"label": "User Password",
|
||||||
@ -1822,6 +1824,7 @@
|
|||||||
"bullet3": "256-bit: Maximum security, requires modern viewers"
|
"bullet3": "256-bit: Maximum security, requires modern viewers"
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
|
"title": "Change Permissions",
|
||||||
"text": "These permissions control what users can do with the PDF. Most effective when combined with an owner password."
|
"text": "These permissions control what users can do with the PDF. Most effective when combined with an owner password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1831,7 +1834,6 @@
|
|||||||
"desc": "Change document restrictions and permissions.",
|
"desc": "Change document restrictions and permissions.",
|
||||||
"completed": "Permissions changed",
|
"completed": "Permissions changed",
|
||||||
"submit": "Change Permissions",
|
"submit": "Change Permissions",
|
||||||
"title": "Document Permissions",
|
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "An error occurred while changing PDF permissions."
|
"failed": "An error occurred while changing PDF permissions."
|
||||||
},
|
},
|
||||||
@ -1875,5 +1877,27 @@
|
|||||||
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password."
|
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"removePassword": {
|
||||||
|
"title": "Remove Password",
|
||||||
|
"desc": "Remove password protection from your PDF document.",
|
||||||
|
"tags": "secure,Decrypt,security,unpassword,delete password",
|
||||||
|
"password": {
|
||||||
|
"stepTitle": "Remove Password",
|
||||||
|
"label": "Current Password",
|
||||||
|
"placeholder": "Enter current password",
|
||||||
|
"completed": "Password configured"
|
||||||
|
},
|
||||||
|
"filenamePrefix": "decrypted",
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while removing the password from the PDF."
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"description": "Removing password protection requires the password that was used to encrypt the PDF. This will decrypt the document, making it accessible without a password."
|
||||||
|
},
|
||||||
|
"submit": "Remove Password",
|
||||||
|
"results": {
|
||||||
|
"title": "Decrypted PDFs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,11 +35,20 @@
|
|||||||
"true": "True",
|
"true": "True",
|
||||||
"false": "False",
|
"false": "False",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
|
"app": {
|
||||||
|
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||||
|
},
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveToBrowser": "Save to Browser",
|
"saveToBrowser": "Save to Browser",
|
||||||
|
"download": "Download",
|
||||||
|
"editYourNewFiles": "Edit your new file(s)",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"fileSelected": "Selected: {{filename}}",
|
"fileSelected": "Selected: {{filename}}",
|
||||||
"filesSelected": "{{count}} files selected",
|
"filesSelected": "{{count}} files selected",
|
||||||
|
"files": {
|
||||||
|
"title": "Files",
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
"noFavourites": "No favorites added",
|
"noFavourites": "No favorites added",
|
||||||
"downloadComplete": "Download Complete",
|
"downloadComplete": "Download Complete",
|
||||||
"bored": "Bored Waiting?",
|
"bored": "Bored Waiting?",
|
||||||
@ -119,6 +128,7 @@
|
|||||||
"page": "Page",
|
"page": "Page",
|
||||||
"pages": "Pages",
|
"pages": "Pages",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
|
"review": "Review",
|
||||||
"addToDoc": "Add to Document",
|
"addToDoc": "Add to Document",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
@ -752,16 +762,6 @@
|
|||||||
"removePages": {
|
"removePages": {
|
||||||
"tags": "Remove pages,delete pages"
|
"tags": "Remove pages,delete pages"
|
||||||
},
|
},
|
||||||
"removePassword": {
|
|
||||||
"tags": "secure,Decrypt,security,unpassword,delete password",
|
|
||||||
"title": "Remove password",
|
|
||||||
"header": "Remove password (Decrypt)",
|
|
||||||
"selectText": {
|
|
||||||
"1": "Select PDF to Decrypt",
|
|
||||||
"2": "Password"
|
|
||||||
},
|
|
||||||
"submit": "Remove"
|
|
||||||
},
|
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
"tags": "squish,small,tiny"
|
"tags": "squish,small,tiny"
|
||||||
},
|
},
|
||||||
@ -804,6 +804,7 @@
|
|||||||
"ocr": {
|
"ocr": {
|
||||||
"tags": "recognition,text,image,scan,read,identify,detection,editable",
|
"tags": "recognition,text,image,scan,read,identify,detection,editable",
|
||||||
"title": "OCR / Scan Cleanup",
|
"title": "OCR / Scan Cleanup",
|
||||||
|
"desc": "Cleanup scans and detects text from images within a PDF and re-adds it as text.",
|
||||||
"header": "Cleanup Scans / OCR (Optical Character Recognition)",
|
"header": "Cleanup Scans / OCR (Optical Character Recognition)",
|
||||||
"selectText": {
|
"selectText": {
|
||||||
"1": "Select languages that are to be detected within the PDF (Ones listed are the ones currently detected):",
|
"1": "Select languages that are to be detected within the PDF (Ones listed are the ones currently detected):",
|
||||||
@ -1635,6 +1636,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addPassword": {
|
"addPassword": {
|
||||||
|
"title": "Add Password",
|
||||||
|
"desc": "Encrypt your PDF document with a password.",
|
||||||
"completed": "Password protection applied",
|
"completed": "Password protection applied",
|
||||||
"submit": "Encrypt",
|
"submit": "Encrypt",
|
||||||
"filenamePrefix": "encrypted",
|
"filenamePrefix": "encrypted",
|
||||||
@ -1686,6 +1689,7 @@
|
|||||||
"bullet3": "256-bit: Maximum security, requires modern viewers"
|
"bullet3": "256-bit: Maximum security, requires modern viewers"
|
||||||
},
|
},
|
||||||
"permissions": {
|
"permissions": {
|
||||||
|
"title": "Change Permissions",
|
||||||
"text": "These permissions control what users can do with the PDF. Most effective when combined with an owner password."
|
"text": "These permissions control what users can do with the PDF. Most effective when combined with an owner password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1737,5 +1741,27 @@
|
|||||||
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password."
|
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"removePassword": {
|
||||||
|
"title": "Remove Password",
|
||||||
|
"desc": "Remove password protection from your PDF document.",
|
||||||
|
"tags": "secure,Decrypt,security,unpassword,delete password",
|
||||||
|
"password": {
|
||||||
|
"stepTitle": "Remove Password",
|
||||||
|
"label": "Current Password",
|
||||||
|
"placeholder": "Enter current password",
|
||||||
|
"completed": "Password configured"
|
||||||
|
},
|
||||||
|
"filenamePrefix": "decrypted",
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while removing the password from the PDF."
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"description": "Removing password protection requires the password that was used to encrypt the PDF. This will decrypt the document, making it accessible without a password."
|
||||||
|
},
|
||||||
|
"submit": "Remove Password",
|
||||||
|
"results": {
|
||||||
|
"title": "Decrypted PDFs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ const LandingPage = () => {
|
|||||||
activateOnClick={false}
|
activateOnClick={false}
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
'&[data-accept]': {
|
'&[dataAccept]': {
|
||||||
backgroundColor: 'var(--landing-drop-paper-bg)',
|
backgroundColor: 'var(--landing-drop-paper-bg)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,127 @@
|
|||||||
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import RemovePasswordSettings from './RemovePasswordSettings';
|
||||||
|
import { defaultParameters } from '../../../hooks/tools/removePassword/useRemovePasswordParameters';
|
||||||
|
|
||||||
|
// Mock useTranslation with predictable return values
|
||||||
|
const mockT = vi.fn((key: string) => `mock-${key}`);
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({ t: mockT })
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Wrapper component to provide Mantine context
|
||||||
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<MantineProvider>{children}</MantineProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('RemovePasswordSettings', () => {
|
||||||
|
const mockOnParameterChange = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render password input field', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('mock-removePassword.password.label')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call onParameterChange when password is entered', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordInput = screen.getByPlaceholderText('mock-removePassword.password.placeholder');
|
||||||
|
fireEvent.change(passwordInput, { target: { value: 'test-password' } });
|
||||||
|
|
||||||
|
expect(mockOnParameterChange).toHaveBeenCalledWith('password', 'test-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display current password value', () => {
|
||||||
|
const parametersWithPassword = { ...defaultParameters, password: 'current-password' };
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={parametersWithPassword}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordInput = screen.getByPlaceholderText('mock-removePassword.password.placeholder') as HTMLInputElement;
|
||||||
|
expect(passwordInput.value).toBe('current-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable password input when disabled prop is true', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordInput = screen.getByPlaceholderText('mock-removePassword.password.placeholder');
|
||||||
|
expect(passwordInput).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should enable password input when disabled prop is false', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordInput = screen.getByPlaceholderText('mock-removePassword.password.placeholder');
|
||||||
|
expect(passwordInput).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show password input as required', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
const passwordInput = screen.getByPlaceholderText('mock-removePassword.password.placeholder');
|
||||||
|
expect(passwordInput).toHaveAttribute('required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call translation function with correct keys', () => {
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={defaultParameters}
|
||||||
|
onParameterChange={mockOnParameterChange}
|
||||||
|
/>
|
||||||
|
</TestWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockT).toHaveBeenCalledWith('removePassword.password.label', 'Current Password');
|
||||||
|
expect(mockT).toHaveBeenCalledWith('removePassword.password.placeholder', 'Enter current password');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Stack, Text, PasswordInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { RemovePasswordParameters } from "../../../hooks/tools/removePassword/useRemovePasswordParameters";
|
||||||
|
|
||||||
|
interface RemovePasswordSettingsProps {
|
||||||
|
parameters: RemovePasswordParameters;
|
||||||
|
onParameterChange: (key: keyof RemovePasswordParameters, value: string) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemovePasswordSettings = ({ parameters, onParameterChange, disabled = false }: RemovePasswordSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<PasswordInput
|
||||||
|
label={t('removePassword.password.label', 'Current Password')}
|
||||||
|
placeholder={t('removePassword.password.placeholder', 'Enter current password')}
|
||||||
|
value={parameters.password}
|
||||||
|
onChange={(e) => onParameterChange('password', e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemovePasswordSettings;
|
@ -11,9 +11,9 @@ export function SuggestedToolsSection(): React.ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Text size="lg" fw={600}>
|
<Text size="lg" fw={600}>
|
||||||
{t('editYourNewFiles', 'Edit your new File(s)')}
|
{t('editYourNewFiles', 'Edit your new file(s)')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
@ -39,4 +39,4 @@ export function SuggestedToolsSection(): React.ReactElement {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export const useAddPasswordPermissionsTips = (): TooltipContent => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: t("addPassword.tooltip.permissions.title", "Document Permissions")
|
title: t("addPassword.tooltip.permissions.title", "Change Permissions")
|
||||||
},
|
},
|
||||||
tips: [
|
tips: [
|
||||||
{
|
{
|
||||||
|
20
frontend/src/components/tooltips/useRemovePasswordTips.ts
Normal file
20
frontend/src/components/tooltips/useRemovePasswordTips.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '../../types/tips';
|
||||||
|
|
||||||
|
export const useRemovePasswordTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("removePassword.title", "Remove Password")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
description: t(
|
||||||
|
"removePassword.tooltip.description",
|
||||||
|
"Removing password protection requires the current password that was used to encrypt the PDF. This will decrypt the document, making it accessible without a password."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
@ -338,19 +338,19 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
case 'CONSUME_FILES': {
|
case 'CONSUME_FILES': {
|
||||||
const { inputFiles, outputFiles } = action.payload;
|
const { inputFiles, outputFiles } = action.payload;
|
||||||
const unpinnedInputFiles = inputFiles.filter(file => !state.pinnedFiles.has(file));
|
const unpinnedInputFiles = inputFiles.filter(file => !state.pinnedFiles.has(file));
|
||||||
|
|
||||||
// Remove unpinned input files and add output files
|
// Remove unpinned input files and add output files
|
||||||
const newActiveFiles = [
|
const newActiveFiles = [
|
||||||
...state.activeFiles.filter(file => !unpinnedInputFiles.includes(file)),
|
...state.activeFiles.filter(file => !unpinnedInputFiles.includes(file)),
|
||||||
...outputFiles
|
...outputFiles
|
||||||
];
|
];
|
||||||
|
|
||||||
// Update processed files map - remove consumed files, keep pinned ones
|
// Update processed files map - remove consumed files, keep pinned ones
|
||||||
const newProcessedFiles = new Map(state.processedFiles);
|
const newProcessedFiles = new Map(state.processedFiles);
|
||||||
unpinnedInputFiles.forEach(file => {
|
unpinnedInputFiles.forEach(file => {
|
||||||
newProcessedFiles.delete(file);
|
newProcessedFiles.delete(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
activeFiles: newActiveFiles,
|
activeFiles: newActiveFiles,
|
||||||
@ -617,7 +617,7 @@ export function FileContextProvider({
|
|||||||
// File consumption function
|
// File consumption function
|
||||||
const consumeFiles = useCallback(async (inputFiles: File[], outputFiles: File[]): Promise<void> => {
|
const consumeFiles = useCallback(async (inputFiles: File[], outputFiles: File[]): Promise<void> => {
|
||||||
dispatch({ type: 'CONSUME_FILES', payload: { inputFiles, outputFiles } });
|
dispatch({ type: 'CONSUME_FILES', payload: { inputFiles, outputFiles } });
|
||||||
|
|
||||||
// Store new output files if persistence is enabled
|
// Store new output files if persistence is enabled
|
||||||
if (enablePersistence) {
|
if (enablePersistence) {
|
||||||
for (const file of outputFiles) {
|
for (const file of outputFiles) {
|
||||||
@ -625,7 +625,7 @@ export function FileContextProvider({
|
|||||||
const fileId = getFileId(file);
|
const fileId = getFileId(file);
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
try {
|
try {
|
||||||
const thumbnail = await (thumbnailGenerationService as any).generateThumbnail(file);
|
const thumbnail = await (thumbnailGenerationService as any /* FIX ME */).generateThumbnail(file);
|
||||||
const storedFile = await fileStorage.storeFile(file, thumbnail);
|
const storedFile = await fileStorage.storeFile(file, thumbnail);
|
||||||
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||||
} catch (thumbnailError) {
|
} catch (thumbnailError) {
|
||||||
|
@ -49,25 +49,6 @@ describe('useAddPasswordOperation', () => {
|
|||||||
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should configure useToolOperation with correct parameters', () => {
|
|
||||||
renderHook(() => useAddPasswordOperation());
|
|
||||||
|
|
||||||
expect(mockUseToolOperation).toHaveBeenCalledWith({
|
|
||||||
operationType: 'addPassword',
|
|
||||||
endpoint: '/api/v1/security/add-password',
|
|
||||||
buildFormData: expect.any(Function),
|
|
||||||
filePrefix: 'translated-addPassword.filenamePrefix_',
|
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: 'error-handler-function'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the result from useToolOperation', () => {
|
|
||||||
const { result } = renderHook(() => useAddPasswordOperation());
|
|
||||||
|
|
||||||
expect(result.current).toBe(mockToolOperationReturn);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
description: 'with all parameters filled',
|
description: 'with all parameters filled',
|
||||||
|
@ -48,25 +48,6 @@ describe('useChangePermissionsOperation', () => {
|
|||||||
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should configure useToolOperation with correct parameters', () => {
|
|
||||||
renderHook(() => useChangePermissionsOperation());
|
|
||||||
|
|
||||||
expect(mockUseToolOperation).toHaveBeenCalledWith({
|
|
||||||
operationType: 'changePermissions',
|
|
||||||
endpoint: '/api/v1/security/add-password',
|
|
||||||
buildFormData: expect.any(Function),
|
|
||||||
filePrefix: 'permissions_',
|
|
||||||
multiFileEndpoint: false,
|
|
||||||
getErrorMessage: 'error-handler-function'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the result from useToolOperation', () => {
|
|
||||||
const { result } = renderHook(() => useChangePermissionsOperation());
|
|
||||||
|
|
||||||
expect(result.current).toBe(mockToolOperationReturn);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
{
|
{
|
||||||
preventAssembly: false,
|
preventAssembly: false,
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useRemovePasswordOperation } from './useRemovePasswordOperation';
|
||||||
|
import type { RemovePasswordParameters } from './useRemovePasswordParameters';
|
||||||
|
|
||||||
|
// Mock the useToolOperation hook
|
||||||
|
vi.mock('../shared/useToolOperation', () => ({
|
||||||
|
useToolOperation: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the translation hook
|
||||||
|
const mockT = vi.fn((key: string) => `translated-${key}`);
|
||||||
|
vi.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({ t: mockT })
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the error handler
|
||||||
|
vi.mock('../../../utils/toolErrorHandler', () => ({
|
||||||
|
createStandardErrorHandler: vi.fn(() => 'error-handler-function')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import the mocked function
|
||||||
|
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
|
||||||
|
|
||||||
|
describe('useRemovePasswordOperation', () => {
|
||||||
|
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||||
|
|
||||||
|
const getToolConfig = (): ToolOperationConfig<RemovePasswordParameters> => mockUseToolOperation.mock.calls[0][0];
|
||||||
|
|
||||||
|
const mockToolOperationReturn: ToolOperationHook<unknown> = {
|
||||||
|
files: [],
|
||||||
|
thumbnails: [],
|
||||||
|
downloadUrl: null,
|
||||||
|
downloadFilename: '',
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
status: '',
|
||||||
|
isGeneratingThumbnails: false,
|
||||||
|
progress: null,
|
||||||
|
executeOperation: vi.fn(),
|
||||||
|
resetResults: vi.fn(),
|
||||||
|
clearError: vi.fn(),
|
||||||
|
cancelOperation: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
description: 'with valid password',
|
||||||
|
password: 'test-password'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with complex password',
|
||||||
|
password: 'C0mpl3x@P@ssw0rd!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with single character password',
|
||||||
|
password: 'a'
|
||||||
|
}
|
||||||
|
])('should create form data correctly $description', ({ password }) => {
|
||||||
|
renderHook(() => useRemovePasswordOperation());
|
||||||
|
|
||||||
|
const callArgs = getToolConfig();
|
||||||
|
const buildFormData = callArgs.buildFormData;
|
||||||
|
|
||||||
|
const testParameters: RemovePasswordParameters = {
|
||||||
|
password
|
||||||
|
};
|
||||||
|
|
||||||
|
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
|
||||||
|
const formData = buildFormData(testParameters, testFile as any);
|
||||||
|
|
||||||
|
// Verify the form data contains the file
|
||||||
|
expect(formData.get('fileInput')).toBe(testFile);
|
||||||
|
|
||||||
|
// Verify password parameter
|
||||||
|
expect(formData.get('password')).toBe(password);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use correct translation for error messages', () => {
|
||||||
|
renderHook(() => useRemovePasswordOperation());
|
||||||
|
|
||||||
|
expect(mockT).toHaveBeenCalledWith(
|
||||||
|
'removePassword.error.failed',
|
||||||
|
'An error occurred while removing the password from the PDF.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ property: 'multiFileEndpoint' as const, expectedValue: false },
|
||||||
|
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/remove-password' },
|
||||||
|
{ property: 'filePrefix' as const, expectedValue: 'translated-removePassword.filenamePrefix_' },
|
||||||
|
{ property: 'operationType' as const, expectedValue: 'removePassword' }
|
||||||
|
])('should configure $property correctly', ({ property, expectedValue }) => {
|
||||||
|
renderHook(() => useRemovePasswordOperation());
|
||||||
|
|
||||||
|
const callArgs = getToolConfig();
|
||||||
|
expect(callArgs[property]).toBe(expectedValue);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { RemovePasswordParameters } from './useRemovePasswordParameters';
|
||||||
|
|
||||||
|
export const useRemovePasswordOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const buildFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("password", parameters.password);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useToolOperation<RemovePasswordParameters>({
|
||||||
|
operationType: 'removePassword',
|
||||||
|
endpoint: '/api/v1/security/remove-password',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,81 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useRemovePasswordParameters, defaultParameters } from './useRemovePasswordParameters';
|
||||||
|
|
||||||
|
describe('useRemovePasswordParameters', () => {
|
||||||
|
test('should initialize with default parameters', () => {
|
||||||
|
const { result } = renderHook(() => useRemovePasswordParameters());
|
||||||
|
|
||||||
|
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update password parameter', () => {
|
||||||
|
const { result } = renderHook(() => useRemovePasswordParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('password', 'test-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.password).toBe('test-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reset parameters to defaults', () => {
|
||||||
|
const { result } = renderHook(() => useRemovePasswordParameters());
|
||||||
|
|
||||||
|
// First, change the password
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('password', 'test-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters.password).toBe('test-password');
|
||||||
|
|
||||||
|
// Then reset
|
||||||
|
act(() => {
|
||||||
|
result.current.resetParameters();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return correct endpoint name', () => {
|
||||||
|
const { result } = renderHook(() => useRemovePasswordParameters());
|
||||||
|
|
||||||
|
expect(result.current.getEndpointName()).toBe('remove-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
description: 'with valid password',
|
||||||
|
password: 'valid-password',
|
||||||
|
expectedValid: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with empty password',
|
||||||
|
password: '',
|
||||||
|
expectedValid: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with whitespace only password',
|
||||||
|
password: ' \t ',
|
||||||
|
expectedValid: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with password containing special characters',
|
||||||
|
password: 'p@ssw0rd!',
|
||||||
|
expectedValid: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'with single character password',
|
||||||
|
password: 'a',
|
||||||
|
expectedValid: true
|
||||||
|
}
|
||||||
|
])('should validate parameters correctly $description', ({ password, expectedValid }) => {
|
||||||
|
const { result } = renderHook(() => useRemovePasswordParameters());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.updateParameter('password', password);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.validateParameters()).toBe(expectedValid);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,49 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export interface RemovePasswordParameters {
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemovePasswordParametersHook {
|
||||||
|
parameters: RemovePasswordParameters;
|
||||||
|
updateParameter: <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => void;
|
||||||
|
resetParameters: () => void;
|
||||||
|
validateParameters: () => boolean;
|
||||||
|
getEndpointName: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: RemovePasswordParameters = {
|
||||||
|
password: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemovePasswordParameters = (): RemovePasswordParametersHook => {
|
||||||
|
const [parameters, setParameters] = useState<RemovePasswordParameters>(defaultParameters);
|
||||||
|
|
||||||
|
const updateParameter = <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => {
|
||||||
|
setParameters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[parameter]: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetParameters = () => {
|
||||||
|
setParameters(defaultParameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateParameters = () => {
|
||||||
|
return parameters.password !== '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEndpointName = () => {
|
||||||
|
return 'remove-password';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters,
|
||||||
|
getEndpointName,
|
||||||
|
};
|
||||||
|
};
|
@ -6,8 +6,9 @@ import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
|||||||
import ApiIcon from "@mui/icons-material/Api";
|
import ApiIcon from "@mui/icons-material/Api";
|
||||||
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
|
||||||
import LockIcon from "@mui/icons-material/Lock";
|
import LockIcon from "@mui/icons-material/Lock";
|
||||||
|
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
import { Tool, ToolDefinition, ToolRegistry } from "../types/tool";
|
||||||
|
|
||||||
|
|
||||||
// Add entry here with maxFiles, endpoints, and lazy component
|
// Add entry here with maxFiles, endpoints, and lazy component
|
||||||
@ -104,6 +105,15 @@ const toolDefinitions: Record<string, ToolDefinition> = {
|
|||||||
description: "Change document restrictions and permissions",
|
description: "Change document restrictions and permissions",
|
||||||
endpoints: ["add-password"]
|
endpoints: ["add-password"]
|
||||||
},
|
},
|
||||||
|
removePassword: {
|
||||||
|
id: "removePassword",
|
||||||
|
icon: <LockOpenIcon />,
|
||||||
|
component: React.lazy(() => import("../tools/RemovePassword")),
|
||||||
|
maxFiles: -1,
|
||||||
|
category: "security",
|
||||||
|
description: "Remove password protection from PDF files",
|
||||||
|
endpoints: ["remove-password"]
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
disabled={endpointLoading}
|
disabled={endpointLoading}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
tooltip: addPasswordPermissionsTips,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
executeButton: {
|
executeButton: {
|
||||||
|
98
frontend/src/tools/RemovePassword.tsx
Normal file
98
frontend/src/tools/RemovePassword.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
||||||
|
|
||||||
|
import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters";
|
||||||
|
import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation";
|
||||||
|
import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const removePasswordParams = useRemovePasswordParameters();
|
||||||
|
const removePasswordOperation = useRemovePasswordOperation();
|
||||||
|
const removePasswordTips = useRemovePasswordTips();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removePasswordParams.getEndpointName());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
removePasswordOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [removePasswordParams.parameters]);
|
||||||
|
|
||||||
|
const handleRemovePassword = async () => {
|
||||||
|
try {
|
||||||
|
await removePasswordOperation.executeOperation(removePasswordParams.parameters, selectedFiles);
|
||||||
|
if (removePasswordOperation.files && onComplete) {
|
||||||
|
onComplete(removePasswordOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("removePassword.error.failed", "Remove password operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "removePassword");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
removePasswordOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("removePassword");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = removePasswordOperation.files.length > 0 || removePasswordOperation.downloadUrl !== null;
|
||||||
|
const passwordCollapsed = !hasFiles || hasResults;
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: t("removePassword.password.stepTitle", "Remove Password"),
|
||||||
|
isCollapsed: passwordCollapsed,
|
||||||
|
onCollapsedClick: hasResults ? handleSettingsReset : undefined,
|
||||||
|
tooltip: removePasswordTips,
|
||||||
|
content: (
|
||||||
|
<RemovePasswordSettings
|
||||||
|
parameters={removePasswordParams.parameters}
|
||||||
|
onParameterChange={removePasswordParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("removePassword.submit", "Remove Password"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleRemovePassword,
|
||||||
|
disabled: !removePasswordParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: removePasswordOperation,
|
||||||
|
title: t("removePassword.results.title", "Decrypted PDFs"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemovePassword;
|
@ -5,18 +5,19 @@
|
|||||||
import { ProcessedFile } from './processing';
|
import { ProcessedFile } from './processing';
|
||||||
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
||||||
|
|
||||||
export type ModeType =
|
export type ModeType =
|
||||||
| 'viewer'
|
| 'viewer'
|
||||||
| 'pageEditor'
|
| 'pageEditor'
|
||||||
| 'fileEditor'
|
| 'fileEditor'
|
||||||
| 'merge'
|
| 'merge'
|
||||||
| 'split'
|
| 'split'
|
||||||
| 'compress'
|
| 'compress'
|
||||||
| 'ocr'
|
| 'ocr'
|
||||||
| 'convert'
|
| 'convert'
|
||||||
| 'sanitize'
|
| 'sanitize'
|
||||||
| 'addPassword'
|
| 'addPassword'
|
||||||
| 'changePermissions';
|
| 'changePermissions'
|
||||||
|
| 'removePassword';
|
||||||
|
|
||||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||||
|
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import { getDocument } from "pdfjs-dist";
|
import { getDocument } from "pdfjs-dist";
|
||||||
|
|
||||||
|
interface ColorScheme {
|
||||||
|
bgTop: string;
|
||||||
|
bgBottom: string;
|
||||||
|
border: string;
|
||||||
|
icon: string;
|
||||||
|
badge: string;
|
||||||
|
textPrimary: string;
|
||||||
|
textSecondary: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate thumbnail scale based on file size
|
* Calculate thumbnail scale based on file size
|
||||||
* Smaller files get higher quality, larger files get lower quality
|
* Smaller files get higher quality, larger files get lower quality
|
||||||
@ -14,6 +24,54 @@ export function calculateScaleFromFileSize(fileSize: number): number {
|
|||||||
return 0.15; // 30MB+: Low quality
|
return 0.15; // 30MB+: Low quality
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate encrypted PDF thumbnail with lock icon
|
||||||
|
*/
|
||||||
|
function generateEncryptedPDFThumbnail(file: File): string {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 120;
|
||||||
|
canvas.height = 150;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
// Use PDF color scheme but with encrypted styling
|
||||||
|
const colorScheme = getFileTypeColorScheme('PDF');
|
||||||
|
|
||||||
|
// Create gradient background
|
||||||
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||||
|
gradient.addColorStop(0, colorScheme.bgTop);
|
||||||
|
gradient.addColorStop(1, colorScheme.bgBottom);
|
||||||
|
|
||||||
|
// Rounded rectangle background
|
||||||
|
drawRoundedRect(ctx, 8, 8, canvas.width - 16, canvas.height - 16, 8);
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Border with dashed pattern for encrypted indicator
|
||||||
|
ctx.strokeStyle = colorScheme.border;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]); // Reset dash pattern
|
||||||
|
|
||||||
|
// Large lock icon as main element
|
||||||
|
drawLargeLockIcon(ctx, canvas.width / 2, canvas.height / 2 - 10, colorScheme);
|
||||||
|
|
||||||
|
// "PDF" text under the lock
|
||||||
|
ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
|
ctx.fillStyle = colorScheme.icon;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('PDF', canvas.width / 2, canvas.height / 2 + 35);
|
||||||
|
|
||||||
|
// File size with subtle styling
|
||||||
|
const sizeText = formatFileSize(file.size);
|
||||||
|
ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
|
ctx.fillStyle = colorScheme.textSecondary;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(sizeText, canvas.width / 2, canvas.height - 15);
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate modern placeholder thumbnail with file extension
|
* Generate modern placeholder thumbnail with file extension
|
||||||
*/
|
*/
|
||||||
@ -22,71 +80,71 @@ function generatePlaceholderThumbnail(file: File): string {
|
|||||||
canvas.width = 120;
|
canvas.width = 120;
|
||||||
canvas.height = 150;
|
canvas.height = 150;
|
||||||
const ctx = canvas.getContext('2d')!;
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
// Get file extension for color theming
|
// Get file extension for color theming
|
||||||
const extension = file.name.split('.').pop()?.toUpperCase() || 'FILE';
|
const extension = file.name.split('.').pop()?.toUpperCase() || 'FILE';
|
||||||
const colorScheme = getFileTypeColorScheme(extension);
|
const colorScheme = getFileTypeColorScheme(extension);
|
||||||
|
|
||||||
// Create gradient background
|
// Create gradient background
|
||||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||||
gradient.addColorStop(0, colorScheme.bgTop);
|
gradient.addColorStop(0, colorScheme.bgTop);
|
||||||
gradient.addColorStop(1, colorScheme.bgBottom);
|
gradient.addColorStop(1, colorScheme.bgBottom);
|
||||||
|
|
||||||
// Rounded rectangle background
|
// Rounded rectangle background
|
||||||
drawRoundedRect(ctx, 8, 8, canvas.width - 16, canvas.height - 16, 8);
|
drawRoundedRect(ctx, 8, 8, canvas.width - 16, canvas.height - 16, 8);
|
||||||
ctx.fillStyle = gradient;
|
ctx.fillStyle = gradient;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Subtle shadow/border
|
// Subtle shadow/border
|
||||||
ctx.strokeStyle = colorScheme.border;
|
ctx.strokeStyle = colorScheme.border;
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Modern document icon
|
// Modern document icon
|
||||||
drawModernDocumentIcon(ctx, canvas.width / 2, 45, colorScheme.icon);
|
drawModernDocumentIcon(ctx, canvas.width / 2, 45, colorScheme.icon);
|
||||||
|
|
||||||
// Extension badge
|
// Extension badge
|
||||||
drawExtensionBadge(ctx, canvas.width / 2, canvas.height / 2 + 15, extension, colorScheme);
|
drawExtensionBadge(ctx, canvas.width / 2, canvas.height / 2 + 15, extension, colorScheme);
|
||||||
|
|
||||||
// File size with subtle styling
|
// File size with subtle styling
|
||||||
const sizeText = formatFileSize(file.size);
|
const sizeText = formatFileSize(file.size);
|
||||||
ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
ctx.fillStyle = colorScheme.textSecondary;
|
ctx.fillStyle = colorScheme.textSecondary;
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(sizeText, canvas.width / 2, canvas.height - 15);
|
ctx.fillText(sizeText, canvas.width / 2, canvas.height - 15);
|
||||||
|
|
||||||
return canvas.toDataURL();
|
return canvas.toDataURL();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get color scheme based on file extension
|
* Get color scheme based on file extension
|
||||||
*/
|
*/
|
||||||
function getFileTypeColorScheme(extension: string) {
|
function getFileTypeColorScheme(extension: string): ColorScheme {
|
||||||
const schemes: Record<string, any> = {
|
const schemes: Record<string, ColorScheme> = {
|
||||||
// Documents
|
// Documents
|
||||||
'PDF': { bgTop: '#FF6B6B20', bgBottom: '#FF6B6B10', border: '#FF6B6B40', icon: '#FF6B6B', badge: '#FF6B6B', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'PDF': { bgTop: '#FF6B6B20', bgBottom: '#FF6B6B10', border: '#FF6B6B40', icon: '#FF6B6B', badge: '#FF6B6B', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'DOC': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'DOC': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'DOCX': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'DOCX': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'TXT': { bgTop: '#95A5A620', bgBottom: '#95A5A610', border: '#95A5A640', icon: '#95A5A6', badge: '#95A5A6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'TXT': { bgTop: '#95A5A620', bgBottom: '#95A5A610', border: '#95A5A640', icon: '#95A5A6', badge: '#95A5A6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
|
|
||||||
// Spreadsheets
|
// Spreadsheets
|
||||||
'XLS': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'XLS': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'XLSX': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'XLSX': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'CSV': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'CSV': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
|
|
||||||
// Presentations
|
// Presentations
|
||||||
'PPT': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'PPT': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'PPTX': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'PPTX': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
|
|
||||||
// Archives
|
// Archives
|
||||||
'ZIP': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'ZIP': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'RAR': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'RAR': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
'7Z': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
'7Z': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
|
||||||
|
|
||||||
// Default
|
// Default
|
||||||
'DEFAULT': { bgTop: '#74B9FF20', bgBottom: '#74B9FF10', border: '#74B9FF40', icon: '#74B9FF', badge: '#74B9FF', textPrimary: '#FFFFFF', textSecondary: '#666666' }
|
'DEFAULT': { bgTop: '#74B9FF20', bgBottom: '#74B9FF10', border: '#74B9FF40', icon: '#74B9FF', badge: '#74B9FF', textPrimary: '#FFFFFF', textSecondary: '#666666' }
|
||||||
};
|
};
|
||||||
|
|
||||||
return schemes[extension] || schemes['DEFAULT'];
|
return schemes[extension] || schemes['DEFAULT'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,11 +173,11 @@ function drawModernDocumentIcon(ctx: CanvasRenderingContext2D, centerX: number,
|
|||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
|
|
||||||
// Document body
|
// Document body
|
||||||
drawRoundedRect(ctx, centerX - size/2, centerY - size/2, size, size * 1.2, 3);
|
drawRoundedRect(ctx, centerX - size/2, centerY - size/2, size, size * 1.2, 3);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Folded corner
|
// Folded corner
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(centerX + size/2 - 6, centerY - size/2);
|
ctx.moveTo(centerX + size/2 - 6, centerY - size/2);
|
||||||
@ -130,18 +188,73 @@ function drawModernDocumentIcon(ctx: CanvasRenderingContext2D, centerX: number,
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw large lock icon for encrypted PDFs
|
||||||
|
*/
|
||||||
|
function drawLargeLockIcon(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, colorScheme: ColorScheme) {
|
||||||
|
const size = 48;
|
||||||
|
ctx.fillStyle = colorScheme.icon;
|
||||||
|
ctx.strokeStyle = colorScheme.icon;
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
|
||||||
|
// Lock body (rectangle)
|
||||||
|
const bodyWidth = size;
|
||||||
|
const bodyHeight = size * 0.75;
|
||||||
|
const bodyX = centerX - bodyWidth / 2;
|
||||||
|
const bodyY = centerY - bodyHeight / 4;
|
||||||
|
|
||||||
|
drawRoundedRect(ctx, bodyX, bodyY, bodyWidth, bodyHeight, 4);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Lock shackle (semicircle)
|
||||||
|
const shackleRadius = size * 0.32;
|
||||||
|
const shackleY = centerY - size * 0.25;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, shackleY, shackleRadius, Math.PI, 2 * Math.PI);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Keyhole
|
||||||
|
const keyholeX = centerX;
|
||||||
|
const keyholeY = bodyY + bodyHeight * 0.4;
|
||||||
|
ctx.fillStyle = colorScheme.textPrimary;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(keyholeX, keyholeY, 4, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillRect(keyholeX - 2, keyholeY, 4, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate standard PDF thumbnail by rendering first page
|
||||||
|
*/
|
||||||
|
async function generateStandardPDFThumbnail(pdf: any, scale: number): Promise<string> {
|
||||||
|
const page = await pdf.getPage(1);
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('Could not get canvas context');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
return canvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw extension badge
|
* Draw extension badge
|
||||||
*/
|
*/
|
||||||
function drawExtensionBadge(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, extension: string, colorScheme: any) {
|
function drawExtensionBadge(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, extension: string, colorScheme: ColorScheme) {
|
||||||
const badgeWidth = Math.max(extension.length * 8 + 16, 40);
|
const badgeWidth = Math.max(extension.length * 8 + 16, 40);
|
||||||
const badgeHeight = 22;
|
const badgeHeight = 22;
|
||||||
|
|
||||||
// Badge background
|
// Badge background
|
||||||
drawRoundedRect(ctx, centerX - badgeWidth/2, centerY - badgeHeight/2, badgeWidth, badgeHeight, 11);
|
drawRoundedRect(ctx, centerX - badgeWidth/2, centerY - badgeHeight/2, badgeWidth, badgeHeight, 11);
|
||||||
ctx.fillStyle = colorScheme.badge;
|
ctx.fillStyle = colorScheme.badge;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Badge text
|
// Badge text
|
||||||
ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
||||||
ctx.fillStyle = colorScheme.textPrimary;
|
ctx.fillStyle = colorScheme.textPrimary;
|
||||||
@ -160,6 +273,29 @@ function formatFileSize(bytes: number): string {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generatePDFThumbnail(arrayBuffer: ArrayBuffer, file: File, scale: number): Promise<string> {
|
||||||
|
try {
|
||||||
|
const pdf = await getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
disableAutoFetch: true,
|
||||||
|
disableStream: true
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
const thumbnail = await generateStandardPDFThumbnail(pdf, scale);
|
||||||
|
|
||||||
|
// Immediately clean up memory after thumbnail generation
|
||||||
|
pdf.destroy();
|
||||||
|
return thumbnail;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// Check if PDF is encrypted
|
||||||
|
if (error.name === "PasswordException") {
|
||||||
|
return generateEncryptedPDFThumbnail(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error; // Not an encryption issue, re-throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate thumbnail for any file type
|
* Generate thumbnail for any file type
|
||||||
@ -187,77 +323,27 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
|||||||
console.log('Generating thumbnail for', file.name);
|
console.log('Generating thumbnail for', file.name);
|
||||||
const scale = calculateScaleFromFileSize(file.size);
|
const scale = calculateScaleFromFileSize(file.size);
|
||||||
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||||||
|
|
||||||
|
// Only read first 2MB for thumbnail generation to save memory
|
||||||
|
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||||
|
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
||||||
|
const arrayBuffer = await chunk.arrayBuffer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Only read first 2MB for thumbnail generation to save memory
|
return await generatePDFThumbnail(arrayBuffer, file, scale);
|
||||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
|
||||||
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
|
||||||
const arrayBuffer = await chunk.arrayBuffer();
|
|
||||||
|
|
||||||
const pdf = await getDocument({
|
|
||||||
data: arrayBuffer,
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: true
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
const page = await pdf.getPage(1);
|
|
||||||
const viewport = page.getViewport({ scale }); // Dynamic scale based on file size
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('Could not get canvas context');
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
|
||||||
const thumbnail = canvas.toDataURL();
|
|
||||||
|
|
||||||
// Immediately clean up memory after thumbnail generation
|
|
||||||
pdf.destroy();
|
|
||||||
console.log('Thumbnail generated and PDF destroyed for', file.name);
|
|
||||||
|
|
||||||
return thumbnail;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
if (error.name === 'InvalidPDFException') {
|
if (error.name === 'InvalidPDFException') {
|
||||||
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
|
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
|
||||||
// Return a placeholder or try with full file instead of chunk
|
// Return a placeholder or try with full file instead of chunk
|
||||||
try {
|
const fullArrayBuffer = await file.arrayBuffer();
|
||||||
const fullArrayBuffer = await file.arrayBuffer();
|
return await generatePDFThumbnail(fullArrayBuffer, file, scale);
|
||||||
const pdf = await getDocument({
|
|
||||||
data: fullArrayBuffer,
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: true,
|
|
||||||
verbosity: 0 // Reduce PDF.js warnings
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
const page = await pdf.getPage(1);
|
|
||||||
const viewport = page.getViewport({ scale });
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('Could not get canvas context');
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
|
||||||
const thumbnail = canvas.toDataURL();
|
|
||||||
|
|
||||||
pdf.destroy();
|
|
||||||
return thumbnail;
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.warn('Fallback thumbnail generation also failed for', file.name, fallbackError);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Failed to generate thumbnail for', file.name, error);
|
console.warn('Unknown error thrown. Failed to generate thumbnail for', file.name, error);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw error; // Re-throw non-Error exceptions
|
||||||
}
|
}
|
||||||
console.warn('Unknown error generating thumbnail for', file.name, error);
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user