Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,14 @@
"saveError": "Failed to save PDF. Please use the export button in the toolbar.",
"savingMessage": "Saving PDF with annotations...",
"successMessage": "PDF saved successfully! Click the download button to save your edited file.",
"textHistory": "Text change history",
"textUndo": "Undo text",
"textRedo": "Redo text",
"textUndoApplied": "The last text change was undone.",
"textRedoApplied": "The text change was redone.",
"overflowAppliedWarning": "The replacement text exceeds the original area. Review the result visually before exporting.",
"fallbackFontWarning": "The original font could not be reused and a fallback was used. Review the characters and spacing.",
"signatureInvalidatedWarning": "This PDF contained digital signatures. Editing its content invalidates the existing signatures.",
"redactWarningTitle": "Redaction Mode Active",
"redactWarningDescription": "Select areas to permanently remove content. Redacted content cannot be recovered after saving.",
"toolbar": {
Expand Down
8 changes: 8 additions & 0 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1754,6 +1754,14 @@
"saveError": "No se pudo guardar el PDF. Utilice el botón exportar en la barra de herramientas.",
"savingMessage": "Guardando PDF con anotaciones...",
"successMessage": "¡PDF guardado exitosamente! Haga clic en el botón de descarga para guardar su archivo editado.",
"textHistory": "Historial de cambios de texto",
"textUndo": "Deshacer texto",
"textRedo": "Rehacer texto",
"textUndoApplied": "Se ha deshecho el último cambio de texto.",
"textRedoApplied": "Se ha rehecho el cambio de texto.",
"overflowAppliedWarning": "El texto sustituido excede el área original. Revise visualmente el resultado antes de exportar.",
"fallbackFontWarning": "La fuente original no pudo reutilizarse y se empleó una alternativa. Revise los caracteres y el espaciado.",
"signatureInvalidatedWarning": "Este PDF contenía firmas digitales. La edición del contenido invalida sus firmas existentes.",
"redactWarningTitle": "Modo de redacción activo",
"redactWarningDescription": "Seleccione áreas para eliminar contenido permanentemente. El contenido redactado no se puede recuperar después de guardarlo.",
"toolbar": {
Expand Down
138 changes: 138 additions & 0 deletions src/__tests__/lib/pdf/replace-existing-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
replaceExistingText,
} from '@/lib/pdf/processors/replace-existing-text';
import type { TextMatch } from '@/lib/pdf/processors/find-and-redact';
import { loadPyMuPDF } from '@/lib/pdf/pymupdf-loader';

vi.mock('@/lib/pdf/pymupdf-loader', () => ({
loadPyMuPDF: vi.fn(),
}));

const matches: TextMatch[] = [
{
page: 1,
text: 'Original',
x: 10,
y: 20,
width: 50,
height: 12,
id: 'first',
selected: true,
},
{
page: 2,
text: 'Original',
x: 15,
y: 25,
width: 50,
height: 12,
id: 'second',
selected: false,
},
];

describe('replaceExistingText', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('passes only selected coordinates to the local PyMuPDF engine', async () => {
const output = new Blob(['edited'], { type: 'application/pdf' });
const diagnostics = {
usedFallbackFont: false,
overflowDetected: false,
hasDigitalSignatures: false,
};
const replaceExistingTextMock = vi.fn().mockResolvedValue({
blob: output,
diagnostics,
});
vi.mocked(loadPyMuPDF).mockResolvedValue({
replaceExistingText: replaceExistingTextMock,
});
const file = new File(['pdf'], 'sample.pdf', { type: 'application/pdf' });

const result = await replaceExistingText(file, matches, {
replacementText: 'Updated',
selectedMatchIds: ['first'],
});

expect(result).toEqual({
success: true,
result: output,
replacedCount: 1,
diagnostics,
});
expect(replaceExistingTextMock).toHaveBeenCalledWith(
file,
[{ page: 1, x: 10, y: 20, width: 50, height: 12 }],
'Updated',
'preserve'
);
});

it('passes the requested overflow strategy to the engine', async () => {
const output = new Blob(['edited'], { type: 'application/pdf' });
const diagnostics = {
usedFallbackFont: true,
overflowDetected: true,
hasDigitalSignatures: true,
};
const replaceExistingTextMock = vi.fn().mockResolvedValue({
blob: output,
diagnostics,
});
vi.mocked(loadPyMuPDF).mockResolvedValue({
replaceExistingText: replaceExistingTextMock,
});

const result = await replaceExistingText(
new File(['pdf'], 'sample.pdf', { type: 'application/pdf' }),
matches,
{
replacementText: 'A much longer replacement',
selectedMatchIds: ['first'],
fitMode: 'shrink',
}
);

expect(replaceExistingTextMock).toHaveBeenCalledWith(
expect.any(File),
[{ page: 1, x: 10, y: 20, width: 50, height: 12 }],
'A much longer replacement',
'shrink'
);
expect(result.diagnostics).toEqual(diagnostics);
});

it('rejects an empty selection without loading the PDF engine', async () => {
const file = new File(['pdf'], 'sample.pdf', { type: 'application/pdf' });

const result = await replaceExistingText(file, matches, {
replacementText: 'Updated',
selectedMatchIds: ['missing'],
});

expect(result.success).toBe(false);
expect(result.replacedCount).toBe(0);
expect(loadPyMuPDF).not.toHaveBeenCalled();
});

it('reports engine errors without returning a partial PDF', async () => {
vi.mocked(loadPyMuPDF).mockResolvedValue({
replaceExistingText: vi.fn().mockRejectedValue(new Error('WASM failed')),
});
const file = new File(['pdf'], 'sample.pdf', { type: 'application/pdf' });

const result = await replaceExistingText(file, matches, {
replacementText: 'Updated',
});

expect(result).toEqual({
success: false,
replacedCount: 0,
error: 'WASM failed',
});
});
});
Loading