Merge branch 'ribai' of https://gitea.maskantech.in/gitea_admin/pdf into azeem
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
VITE_GATEWAY_URL=https://pdfapi-dev.maskantech.in
|
||||
VITE_GATEWAY_URL=http://127.0.0.1:8765
|
||||
|
||||
+47
-11
@@ -89,14 +89,34 @@ function App() {
|
||||
reorderPage?: (fromIndex: number, toIndex: number) => void;
|
||||
} | null>(null);
|
||||
|
||||
const [mergeInitialFile, setMergeInitialFile] = useState<File | null>(null);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isRemote = urlParams.has('stream_url') && urlParams.has('upload_url') && urlParams.has('token');
|
||||
const urlToken = urlParams.get('token') || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTool === 'create_pdf') {
|
||||
setCreatePdfModalOpen(true);
|
||||
} else if (activeTool === 'merge_pdf') {
|
||||
setMergeModalOpen(true);
|
||||
setActiveTool('select');
|
||||
const handleMergeOpen = async () => {
|
||||
if (isRemote && activeDoc && selectedDocId) {
|
||||
try {
|
||||
const blob = await gatewayService.exportDocumentBlob(selectedDocId);
|
||||
setMergeInitialFile(new File([blob], activeDoc.filename || 'current_document.pdf', { type: 'application/pdf' }));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch initial blob for merge', err);
|
||||
setMergeInitialFile(null);
|
||||
}
|
||||
} else {
|
||||
setMergeInitialFile(null);
|
||||
}
|
||||
setMergeModalOpen(true);
|
||||
setActiveTool('select');
|
||||
};
|
||||
handleMergeOpen();
|
||||
}
|
||||
}, [activeTool]);
|
||||
}, [activeTool, isRemote, activeDoc, selectedDocId]);
|
||||
|
||||
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
||||
const [metadata, setMetadata] = useState<DocumentMetadata | null>(null);
|
||||
@@ -168,10 +188,6 @@ function App() {
|
||||
setHist({ stack: [id], index: 0 });
|
||||
}, []);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isRemote = urlParams.has('stream_url') && urlParams.has('upload_url') && urlParams.has('token');
|
||||
const urlToken = urlParams.get('token') || undefined;
|
||||
|
||||
const hasUnsavedChanges = useCallback(() => {
|
||||
if (activeTool === 'create_pdf' || createPdfModalOpen) {
|
||||
return Boolean(creatorActions?.canUndo);
|
||||
@@ -394,6 +410,8 @@ function App() {
|
||||
.catch(() => setBackendHealthy(false));
|
||||
}, []);
|
||||
|
||||
const hasImportedRemoteRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -404,7 +422,8 @@ function App() {
|
||||
const token = params.get('token');
|
||||
const resourceId = params.get('resource_id');
|
||||
|
||||
if (streamUrl && uploadUrl && token) {
|
||||
if (streamUrl && uploadUrl && token && !hasImportedRemoteRef.current) {
|
||||
hasImportedRemoteRef.current = true;
|
||||
try {
|
||||
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || '';
|
||||
const res = await fetch(`${gatewayUrl}/documents/import-remote`, {
|
||||
@@ -984,7 +1003,14 @@ function App() {
|
||||
setProtectModalState(null);
|
||||
setIsInspectorOpen(true);
|
||||
setInspectorTab('properties');
|
||||
setExportModalOpen(true);
|
||||
|
||||
if (isRemote) {
|
||||
await gatewayService.exportRemoteDocument(targetDocId, urlToken);
|
||||
const parentOrigin = urlParams.get('parent_origin') || import.meta.env.VITE_PARENT_ORIGIN || '*';
|
||||
window.parent.postMessage({ type: 'REMOTE_SAVE_COMPLETE' }, parentOrigin);
|
||||
} else {
|
||||
setExportModalOpen(true);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Protection failed', e);
|
||||
throw e;
|
||||
@@ -1003,7 +1029,14 @@ function App() {
|
||||
setUnlockModalState(null);
|
||||
setIsInspectorOpen(true);
|
||||
setInspectorTab('properties');
|
||||
setExportModalOpen(true);
|
||||
|
||||
if (isRemote) {
|
||||
await gatewayService.exportRemoteDocument(selectedDocId, urlToken);
|
||||
const parentOrigin = urlParams.get('parent_origin') || import.meta.env.VITE_PARENT_ORIGIN || '*';
|
||||
window.parent.postMessage({ type: 'REMOTE_SAVE_COMPLETE' }, parentOrigin);
|
||||
} else {
|
||||
setExportModalOpen(true);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Unlock failed', e);
|
||||
throw e;
|
||||
@@ -1190,7 +1223,7 @@ function App() {
|
||||
}}
|
||||
onUnlock={() => selectedDocId && setUnlockModalState({ documentId: selectedDocId, filename: activeDoc?.filename || 'Document.pdf' })}
|
||||
onCompare={handleOpenCompareModal}
|
||||
onMergePDF={() => setMergeModalOpen(true)}
|
||||
onMergePDF={() => setActiveTool('merge_pdf')}
|
||||
isEncrypted={activeDoc?.permissions?.isEncrypted ?? false}
|
||||
canPrint={(activeTool === 'create_pdf' || createPdfModalOpen) ? true : can('canPrint')}
|
||||
canExport={(activeTool === 'create_pdf' || createPdfModalOpen) ? true : can('canCopy')}
|
||||
@@ -1207,6 +1240,7 @@ function App() {
|
||||
totalPages={activeDoc?.totalPages || 1}
|
||||
onGoToPage={(p) => viewerRef.current?.scrollToPage(p)}
|
||||
onSave={handleSave}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-0 flex-1">
|
||||
@@ -1220,6 +1254,7 @@ function App() {
|
||||
onOpenAbout={() => setAboutModalOpen(true)}
|
||||
disabledTools={disabledTools}
|
||||
isOCRLoading={isOCRLoading}
|
||||
isRemote={isRemote}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
@@ -1598,6 +1633,7 @@ function App() {
|
||||
onClose={() => setMergeModalOpen(false)}
|
||||
onMergeComplete={handleMergeCompleted}
|
||||
apiBaseUrl={import.meta.env.VITE_GATEWAY_URL || 'http://localhost:8000'}
|
||||
initialFile={mergeInitialFile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ interface MergePDFModalProps {
|
||||
onClose: () => void;
|
||||
onMergeComplete: (docInfo: any) => void;
|
||||
apiBaseUrl?: string;
|
||||
initialFile?: File | null;
|
||||
}
|
||||
|
||||
export const MergePDFModal: React.FC<MergePDFModalProps> = ({
|
||||
@@ -22,6 +23,7 @@ export const MergePDFModal: React.FC<MergePDFModalProps> = ({
|
||||
onClose,
|
||||
onMergeComplete,
|
||||
apiBaseUrl = 'http://localhost:8000',
|
||||
initialFile = null,
|
||||
}) => {
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
const [outputFilename, setOutputFilename] = useState<string>('merged_document.pdf');
|
||||
@@ -30,6 +32,25 @@ export const MergePDFModal: React.FC<MergePDFModalProps> = ({
|
||||
const [mergedResult, setMergedResult] = useState<any | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (initialFile) {
|
||||
setFiles([{
|
||||
id: `initial-${Date.now()}`,
|
||||
file: initialFile,
|
||||
pagesMode: 'all',
|
||||
customPages: '',
|
||||
}]);
|
||||
} else {
|
||||
setFiles([]);
|
||||
}
|
||||
setOutputFilename('merged_document.pdf');
|
||||
setError(null);
|
||||
setMergedResult(null);
|
||||
setIsMerging(false);
|
||||
}
|
||||
}, [isOpen, initialFile]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleAddFiles = (selectedFiles: FileList | null) => {
|
||||
|
||||
@@ -112,6 +112,7 @@ interface ToolRailProps {
|
||||
disabledTools?: Set<ToolId>;
|
||||
onRunOCR?: () => void;
|
||||
isOCRLoading?: boolean;
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
const RailButton: React.FC<{ t: ToolDef; active: boolean; disabled?: boolean; isLoading?: boolean; onClick: () => void }> = ({ t, active, disabled, isLoading, onClick }) => (
|
||||
@@ -139,7 +140,7 @@ const RailButton: React.FC<{ t: ToolDef; active: boolean; disabled?: boolean; is
|
||||
</CustomButton>
|
||||
);
|
||||
|
||||
export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools, onRunOCR, isOCRLoading }) => {
|
||||
export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, hasSignature, onOpenSignature, onOpenAbout, disabledTools, onRunOCR, isOCRLoading, isRemote }) => {
|
||||
const pickTool = (id: ToolId) => {
|
||||
if (disabledTools?.has(id)) {
|
||||
return;
|
||||
@@ -151,7 +152,7 @@ export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, ha
|
||||
|
||||
return (
|
||||
<nav className="flex h-full shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-border-primary bg-bg-primary scroll-micro" style={{ width: '92px', paddingTop: '16px', paddingBottom: '16px' }}>
|
||||
{TOOLS.map((t, i) =>
|
||||
{TOOLS.filter(t => !isRemote || t === 'divider' || t.id !== 'create_pdf').map((t, i) =>
|
||||
t === 'divider'
|
||||
? <div key={`d${i}`} className="my-1 h-px w-10 shrink-0 bg-border-primary" />
|
||||
: <RailButton key={t.id} t={t} active={activeTool === t.id} disabled={disabledTools?.has(t.id)} isLoading={t.id === 'ocr' && isOCRLoading} onClick={() => pickTool(t.id)} />,
|
||||
@@ -174,7 +175,7 @@ export const ToolRail: React.FC<ToolRailProps> = ({ activeTool, onToolChange, ha
|
||||
>
|
||||
<div className="text-[12px]">
|
||||
<p className="mb-1.5 px-1 font-bold text-text-primary">Shortcuts</p>
|
||||
{TOOLS.filter((t): t is ToolDef => t !== 'divider').map((t) => (
|
||||
{TOOLS.filter((t): t is ToolDef => t !== 'divider' && (!isRemote || t.id !== 'create_pdf')).map((t) => (
|
||||
<div key={t.id} className="flex items-center justify-between px-1 py-0.5">
|
||||
<span className="text-text-secondary">{t.label}</span>
|
||||
<kbd className="rounded border border-border-primary bg-bg-secondary px-1.5 text-[10px] font-semibold">{t.shortcut}</kbd>
|
||||
|
||||
@@ -39,6 +39,7 @@ interface TopBarProps {
|
||||
canPrint?: boolean;
|
||||
canExport?: boolean;
|
||||
canAssemble?: boolean;
|
||||
isRemote?: boolean;
|
||||
}
|
||||
|
||||
const ZOOM_PRESETS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
|
||||
@@ -47,7 +48,7 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
documentName, backendHealthy, engineReady, zoom, onZoomChange, onFitWidth,
|
||||
currentPage, totalPages, onGoToPage, canUndo, canRedo, onUndo, onRedo,
|
||||
isSaving, isDirtySaved, onRotate, onExport, onPrint, onProtect, onUnlock, onCompare, onMergePDF, isEncrypted, onUpload, onNewBlankPDF, onSave,
|
||||
canPrint = true, canExport = true, canAssemble = true,
|
||||
canPrint = true, canExport = true, canAssemble = true, isRemote = false,
|
||||
}) => {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -81,8 +82,8 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col text-[13px]">
|
||||
<MenuItem icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="12" y1="18" x2="12" y2="12" /><line x1="9" y1="15" x2="15" y2="15" /></svg>} onClick={() => onNewBlankPDF?.()}>New Blank PDF…</MenuItem>
|
||||
<MenuItem icon={<UploadIcon size={16} />} onClick={() => fileRef.current?.click()}>Open PDF…</MenuItem>
|
||||
{!isRemote && <MenuItem icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="12" y1="18" x2="12" y2="12" /><line x1="9" y1="15" x2="15" y2="15" /></svg>} onClick={() => onNewBlankPDF?.()}>New Blank PDF…</MenuItem>}
|
||||
{!isRemote && <MenuItem icon={<UploadIcon size={16} />} onClick={() => fileRef.current?.click()}>Open PDF…</MenuItem>}
|
||||
<MenuItem icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><path d="M8 7v10M12 4v16M16 7v10M4 11h16M4 15h16"/></svg>} onClick={() => onMergePDF?.()}>Merge PDFs…</MenuItem>
|
||||
<MenuItem icon={<DownloadIcon size={16} />} onClick={onExport} disabled={!documentName || !canExport}>Export / Download</MenuItem>
|
||||
<MenuItem icon={<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>} onClick={onPrint} disabled={!documentName || !canPrint}>Print</MenuItem>
|
||||
|
||||
@@ -77,7 +77,7 @@ export const UnsavedChangesModal: React.FC<UnsavedChangesModalProps> = ({ state,
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
variant="secondary"
|
||||
variant="subtle"
|
||||
onClick={handleDiscard}
|
||||
disabled={isSaving}
|
||||
className="rounded-[8px] px-4 font-semibold text-[#dc2626] hover:bg-[#fdecec]"
|
||||
|
||||
@@ -33,7 +33,6 @@ interface DocumentEditorProps {
|
||||
*/
|
||||
const EditableParagraphBlock: React.FC<{
|
||||
block: ParagraphBlock;
|
||||
isSelected: boolean;
|
||||
onSelectBlock: (blockId: string, runId?: string) => void;
|
||||
onUpdateParagraphText: (blockId: string, text: string) => void;
|
||||
onAddParagraph: (afterBlockId?: string) => void;
|
||||
@@ -41,7 +40,6 @@ const EditableParagraphBlock: React.FC<{
|
||||
onFormattingShortcut: (type: 'bold' | 'italic' | 'underline' | 'undo' | 'redo' | 'pageBreak') => void;
|
||||
}> = ({
|
||||
block,
|
||||
isSelected,
|
||||
onSelectBlock,
|
||||
onUpdateParagraphText,
|
||||
onAddParagraph,
|
||||
@@ -782,8 +780,6 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
const activePoints = activeDrawing?.pageIdx === pageIdx ? activeDrawing.points : [];
|
||||
const activeD = activePoints.reduce((acc, pt, i) => (i === 0 ? `M ${pt.x} ${pt.y}` : `${acc} L ${pt.x} ${pt.y}`), '');
|
||||
|
||||
const usableContentHeightPx = pageH - padTop - padBottom - (header.enabled ? 36 * PT_TO_PX : 0) - (footer.enabled ? 36 * PT_TO_PX : 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`page-${page.pageNumber}`}
|
||||
@@ -875,7 +871,6 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
<EditableParagraphBlock
|
||||
key={block.id}
|
||||
block={block as ParagraphBlock}
|
||||
isSelected={isSelected}
|
||||
onSelectBlock={onSelectBlock}
|
||||
onUpdateParagraphText={onUpdateParagraphText}
|
||||
onAddParagraph={onAddParagraph}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { gatewayService } from '../lib/gatewayService';
|
||||
|
||||
interface CanvasLayerProps {
|
||||
@@ -23,11 +23,10 @@ export const CanvasLayer: React.FC<CanvasLayerProps> = ({
|
||||
onRenderComplete,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const [vectorOps, setVectorOps] = useState<any[] | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
gatewayService.getPageDisplayList(documentId, pageIndex)
|
||||
.then(ops => setVectorOps(ops))
|
||||
.catch(console.error);
|
||||
}, [documentId, pageIndex]);
|
||||
|
||||
@@ -66,7 +65,7 @@ export const CanvasLayer: React.FC<CanvasLayerProps> = ({
|
||||
};
|
||||
img.src = imageUrl;
|
||||
|
||||
}, [imageUrl, zoom, rotation, width, height, onRenderComplete, vectorOps]);
|
||||
}, [imageUrl, zoom, rotation, width, height, onRenderComplete]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
||||
@@ -77,8 +77,13 @@ async def export_remote_document(document_id: str, body: ExportRemoteRequest | N
|
||||
)
|
||||
|
||||
try:
|
||||
doc = d["doc_instance"]
|
||||
bytes_data = doc.save_full_for_export()
|
||||
# ALWAYS use the stored bytes_data, which accurately reflects the current state (including encryption).
|
||||
# doc.save_full_for_export() drops encryption or corrupts the output when exporting a protected PDF.
|
||||
bytes_data = d.get("bytes_data")
|
||||
if not bytes_data:
|
||||
doc = d["doc_instance"]
|
||||
bytes_data = doc.save_full_for_export()
|
||||
|
||||
filename = d["filename"]
|
||||
if not filename.endswith(".pdf"):
|
||||
filename += ".pdf"
|
||||
|
||||
Reference in New Issue
Block a user