feat: Improve file preview with Blob URLs
Utilize `URL.createObjectURL` for richer file previews, especially for PDFs. This approach provides better compatibility and performance compared to Base64 previews. Introduces memory leak prevention by revoking Blob URLs when the component unmounts or the app resets.
This commit is contained in:
parent
778caa8a45
commit
d6cab4aeb5
3 changed files with 70 additions and 31 deletions
10
App.tsx
10
App.tsx
|
|
@ -44,6 +44,14 @@ const App: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
// Cleanup blob URLs to prevent memory leaks
|
||||||
|
if (formFile?.previewUrl && formFile.previewUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(formFile.previewUrl);
|
||||||
|
}
|
||||||
|
if (sourceFile?.previewUrl && sourceFile.previewUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(sourceFile.previewUrl);
|
||||||
|
}
|
||||||
|
|
||||||
setStatus(AppStatus.IDLE);
|
setStatus(AppStatus.IDLE);
|
||||||
setFormFile(null);
|
setFormFile(null);
|
||||||
setSourceFile(null);
|
setSourceFile(null);
|
||||||
|
|
@ -223,4 +231,4 @@ const App: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
@ -21,6 +21,9 @@ export const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
|
||||||
const processFile = (file: File) => {
|
const processFile = (file: File) => {
|
||||||
|
// Create a robust Blob URL for previewing (works better than Base64 for PDFs)
|
||||||
|
const objectUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const base64String = reader.result as string;
|
const base64String = reader.result as string;
|
||||||
|
|
@ -29,7 +32,7 @@ export const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
|
|
||||||
onFileSelect({
|
onFileSelect({
|
||||||
file,
|
file,
|
||||||
previewUrl: file.type.startsWith('image/') ? base64String : null,
|
previewUrl: objectUrl,
|
||||||
base64: base64Content,
|
base64: base64Content,
|
||||||
type: file.type as any
|
type: file.type as any
|
||||||
});
|
});
|
||||||
|
|
@ -92,10 +95,10 @@ export const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<div className="relative border border-indigo-100 bg-indigo-50/50 rounded-xl p-4 flex items-center space-x-4">
|
<div className="relative border border-indigo-100 bg-indigo-50/50 rounded-xl p-4 flex items-center space-x-4">
|
||||||
<div className="w-12 h-12 bg-white rounded-lg shadow-sm flex items-center justify-center flex-shrink-0 overflow-hidden">
|
<div className="w-12 h-12 bg-white rounded-lg shadow-sm flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||||
{selectedFile.previewUrl ? (
|
{selectedFile.type === 'application/pdf' ? (
|
||||||
<img src={selectedFile.previewUrl} alt="Preview" className="w-full h-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<FileText className="w-6 h-6 text-indigo-600" />
|
<FileText className="w-6 h-6 text-indigo-600" />
|
||||||
|
) : (
|
||||||
|
<img src={selectedFile.previewUrl!} alt="Preview" className="w-full h-full object-cover" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { ExtractedField, FileData } from '../types';
|
import { ExtractedField, FileData } from '../types';
|
||||||
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, LayoutTemplate, List, Move } from 'lucide-react';
|
import { Check, Edit2, Download, RefreshCw, FileText, AlertTriangle, XCircle, ArrowRight, PenTool, CheckCircle2, Circle, LayoutTemplate, List, Move, ExternalLink } from 'lucide-react';
|
||||||
import { createFilledPdf } from '../services/pdfService';
|
import { createFilledPdf } from '../services/pdfService';
|
||||||
import { jsPDF } from "jspdf";
|
import { jsPDF } from "jspdf";
|
||||||
|
|
||||||
|
|
@ -43,9 +43,10 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
// Generate preview for PDF download
|
// Generate preview for PDF download
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
let timeoutId: any;
|
||||||
|
|
||||||
const generatePreview = async () => {
|
const generatePreview = async () => {
|
||||||
|
// If it's a PDF, we try to show the filled version
|
||||||
if (formFile.type === 'application/pdf') {
|
if (formFile.type === 'application/pdf') {
|
||||||
try {
|
try {
|
||||||
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf);
|
const filledPdfBytes = await createFilledPdf(formFile.base64, fields, isFillablePdf);
|
||||||
|
|
@ -60,14 +61,17 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to generate PDF preview", e);
|
console.error("Failed to generate PDF preview", e);
|
||||||
|
// Fallback to original if generation fails
|
||||||
|
setPreviewUrl(formFile.previewUrl);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// For images, just show the original
|
||||||
setPreviewUrl(formFile.previewUrl);
|
setPreviewUrl(formFile.previewUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounce to avoid excessive PDF generation
|
// Debounce to avoid excessive PDF generation while typing
|
||||||
timeoutId = setTimeout(generatePreview, 600);
|
timeoutId = setTimeout(generatePreview, 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
|
|
@ -79,7 +83,9 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
setPreviewUrl(prev => {
|
setPreviewUrl(prev => {
|
||||||
if (prev && prev.startsWith('blob:')) URL.revokeObjectURL(prev);
|
if (prev && prev.startsWith('blob:') && prev !== formFile.previewUrl) {
|
||||||
|
URL.revokeObjectURL(prev);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -261,24 +267,32 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
|
<div className="bg-slate-900 rounded-xl overflow-hidden shadow-lg flex flex-col">
|
||||||
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
<div className="p-3 bg-slate-800 border-b border-slate-700 flex justify-between items-center">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">PDF Preview</span>
|
<span className="text-xs font-medium text-slate-300 uppercase tracking-wider">PDF Preview (Live)</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
<span className="text-xs text-slate-400">{formFile.file.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 bg-slate-900 relative">
|
<div className="flex-1 bg-slate-900 relative">
|
||||||
{previewUrl ? (
|
{previewUrl ? (
|
||||||
formFile.type === 'application/pdf' ? (
|
formFile.type === 'application/pdf' ? (
|
||||||
<object
|
<>
|
||||||
data={previewUrl}
|
<iframe
|
||||||
type="application/pdf"
|
src={previewUrl}
|
||||||
className="w-full h-full block"
|
title="Form PDF Preview"
|
||||||
aria-label="PDF Preview"
|
className="w-full h-full border-none"
|
||||||
>
|
/>
|
||||||
<div className="flex flex-col items-center justify-center h-full text-white/70">
|
{/* Fallback open button */}
|
||||||
<p>Unable to display PDF directly.</p>
|
<div className="absolute bottom-4 right-4 opacity-50 hover:opacity-100 transition-opacity">
|
||||||
<a href={previewUrl} download className="text-indigo-400 underline mt-2">Download to view</a>
|
<a
|
||||||
|
href={previewUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center bg-black/70 text-white text-xs px-3 py-1.5 rounded-full hover:bg-black"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3 h-3 mr-1" />
|
||||||
|
Open in new tab
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</object>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full overflow-auto flex items-center justify-center p-4">
|
<div className="w-full h-full overflow-auto flex items-center justify-center p-4">
|
||||||
<img
|
<img
|
||||||
|
|
@ -291,7 +305,7 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-slate-500">
|
<div className="flex flex-col items-center justify-center h-full text-slate-500">
|
||||||
<FileText className="w-16 h-16 mb-4 opacity-50" />
|
<FileText className="w-16 h-16 mb-4 opacity-50" />
|
||||||
<p>Preview not available</p>
|
<p>Generating preview...</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -402,10 +416,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="relative bg-white shadow-2xl transition-cursor"
|
className="relative inline-block shadow-2xl transition-cursor bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: '794px',
|
|
||||||
minHeight: '1123px',
|
|
||||||
cursor: draggingField !== null ? 'grabbing' : 'default'
|
cursor: draggingField !== null ? 'grabbing' : 'default'
|
||||||
}}
|
}}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
|
|
@ -413,16 +425,32 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
onMouseLeave={handleMouseUp}
|
onMouseLeave={handleMouseUp}
|
||||||
>
|
>
|
||||||
{/* Background Image/PDF */}
|
{/* Background Image/PDF */}
|
||||||
{formFile.previewUrl && (
|
{formFile.type === 'application/pdf' ? (
|
||||||
|
<div className="flex flex-col items-center justify-center p-20 bg-slate-50 border border-slate-200 text-center" style={{ width: '794px', height: '1123px' }}>
|
||||||
|
<FileText className="w-20 h-20 text-slate-300 mb-4" />
|
||||||
|
<h3 className="text-xl font-bold text-slate-800">Visual Editing Unavailable for PDF</h3>
|
||||||
|
<p className="text-slate-500 max-w-sm mt-2">
|
||||||
|
The visual drag-and-drop editor works best with Image uploads.
|
||||||
|
For this PDF, please use the List View to verify data or Download to see the final result.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('LIST')}
|
||||||
|
className="mt-6 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
Switch to List View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<img
|
<img
|
||||||
src={formFile.previewUrl}
|
src={formFile.previewUrl!}
|
||||||
className="absolute inset-0 w-full h-full object-contain pointer-events-none opacity-90 select-none"
|
className="block max-w-full h-auto pointer-events-none opacity-90 select-none"
|
||||||
|
style={{ maxHeight: '1200px' }}
|
||||||
alt="Form Background"
|
alt="Form Background"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fallback info if not visual mode compatible */}
|
{/* Fallback info if not visual mode compatible */}
|
||||||
{!fields.some(f => f.coordinates) && (
|
{!fields.some(f => f.coordinates) && formFile.type !== 'application/pdf' && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-20 backdrop-blur-sm">
|
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-20 backdrop-blur-sm">
|
||||||
<div className="text-center p-6 max-w-md">
|
<div className="text-center p-6 max-w-md">
|
||||||
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
|
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
|
||||||
|
|
@ -440,8 +468,8 @@ export const ReviewPanel: React.FC<ReviewPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Overlay Inputs */}
|
{/* Overlay Inputs (Only show if not PDF, or if we force it but it's blank bg) */}
|
||||||
{fields.map((field, idx) => {
|
{formFile.type !== 'application/pdf' && fields.map((field, idx) => {
|
||||||
if (!field.coordinates) return null;
|
if (!field.coordinates) return null;
|
||||||
|
|
||||||
// Coordinate conversion (0-1000 scale to percentage)
|
// Coordinate conversion (0-1000 scale to percentage)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue