Merge pull request 'fix_issue' (#87) from fix_issue into docker

Reviewed-on: https://gitea.maskantech.in/gitea_admin/pdf/pulls/87
This commit is contained in:
2026-08-22 10:36:49 +00:00
10 changed files with 382 additions and 408 deletions
+1
View File
@@ -21,6 +21,7 @@ CMakeUserPresets.json
/third_party/pdfium/checkout/
/third_party/pdfium/install/
/third_party/pdfium/.gclient*
/corpus/
# Skia from-source build (depot_tools / GN / Ninja)
/third_party/skia/depot_tools/
+3 -13
View File
@@ -52,20 +52,14 @@ function App() {
const canRedo = hist.index < hist.stack.length - 1;
const preservePageRef = useRef(false);
const [isOcrDisabled, setIsOcrDisabled] = useState(false);
const permissions = activeDoc?.permissions ?? null;
const can = (flag: keyof PDFPermissions) => !permissions || permissions[flag] !== false;
const denyToast = (_label: string) => { };
const disabledTools = new Set<ToolId>();
disabledTools.add('ocr'); // OCR & Edit button is permanently disabled
if (!activeDoc) {
(['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'edit_text', 'stream_edit', 'signature', 'stamp', 'watermark', 'redact'] as ToolId[]).forEach((t) => disabledTools.add(t));
} else {
disabledTools.add('ocr');
if (!can('canAnnotate'))
(['highlight', 'underline', 'strikeout', 'squiggly', 'draw', 'comment', 'textbox', 'stamp', 'signature'] as ToolId[]).forEach((t) => disabledTools.add(t));
if (!can('canModify'))
(['edit_text', 'stream_edit', 'redact'] as ToolId[]).forEach((t) => disabledTools.add(t));
}
if (!can('canModify')) (['edit_text', 'redact'] as ToolId[]).forEach((t) => disabledTools.add(t));
const disabledToolsRef = useRef(disabledTools);
disabledToolsRef.current = disabledTools;
@@ -160,11 +154,7 @@ function App() {
await gatewayService.performPageOCR(activeDoc.id, currentPage);
viewerRef.current?.refreshPageLayout(currentPage);
} catch (err: any) {
const errMsg = err.message || String(err);
alert(`OCR processing failed: ${errMsg}`);
if (errMsg.toLowerCase().includes('not installed') || errMsg.toLowerCase().includes('not available')) {
setIsOcrDisabled(true);
}
alert(`OCR processing failed: ${err.message || err}`);
} finally {
setIsOCRLoading(false);
}
+1 -1
View File
@@ -122,7 +122,7 @@ const RailButton: React.FC<{ t: ToolDef; active: boolean; disabled?: boolean; is
aria-disabled={disabled}
onClick={onClick}
className={`relative flex h-[52px] w-[72px] shrink-0 flex-col items-center justify-center gap-[3px] rounded-[10px] transition-colors ${disabled
? 'cursor-not-allowed text-[#c5cad1]'
? 'cursor-not-allowed text-text-tertiary opacity-40 dark:text-zinc-600 dark:opacity-60'
: active
? t.danger ? 'bg-[#fdecec] text-[#dc2626]' : 'bg-brand-secondary text-brand-primary'
: t.danger ? 'text-text-secondary hover:bg-[#fdecec] hover:text-[#dc2626]'
@@ -60,25 +60,7 @@ const INITIAL_DOC: DocumentModel = {
enabled: true,
showPageNumbers: true,
},
blocks: [
{
id: 'blk-1',
type: 'paragraph',
styleName: 'Normal',
runs: [
{
id: 'run-1',
text: '',
fontFamily: 'Helvetica',
fontSize: 12,
color: '#0f172a',
},
],
alignment: 'left',
spaceBefore: 0,
spaceAfter: 6,
},
],
blocks: [],
};
export const CreatePDFModal: React.FC<CreatePDFModalProps> = ({
@@ -97,8 +79,8 @@ export const CreatePDFModal: React.FC<CreatePDFModalProps> = ({
}) => {
const historyRef = useRef(new HistoryManager(INITIAL_DOC));
const [doc, setDoc] = useState<DocumentModel>(INITIAL_DOC);
const [selectedBlockId, setSelectedBlockId] = useState<string | null>('blk-1');
const [activeRunId, setActiveRunId] = useState<string | null>('run-1');
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
const [activeRunId, setActiveRunId] = useState<string | null>(null);
const [zoomScale, setZoomScale] = useState<number>(1.0);
const [isGenerating, setIsGenerating] = useState(false);
@@ -117,37 +99,12 @@ export const CreatePDFModal: React.FC<CreatePDFModalProps> = ({
const freshDoc: DocumentModel = {
...INITIAL_DOC,
id: `doc-creator-${Date.now()}`,
blocks: [
{
id: 'blk-1',
type: 'paragraph',
styleName: 'Normal',
runs: [
{
id: 'run-1',
text: '',
fontFamily: 'Helvetica',
fontSize: 12,
color: '#0f172a',
},
],
alignment: 'left',
spaceBefore: 0,
spaceAfter: 6,
},
],
blocks: [],
};
historyRef.current = new HistoryManager(freshDoc);
setDoc(freshDoc);
setSelectedBlockId('blk-1');
setActiveRunId('run-1');
setTimeout(() => {
const el = document.querySelector<HTMLDivElement>('[data-block-id="blk-1"]');
if (el) {
el.focus();
}
}, 50);
setSelectedBlockId(null);
setActiveRunId(null);
}
}, [isOpen]);
@@ -331,7 +288,7 @@ export const CreatePDFModal: React.FC<CreatePDFModalProps> = ({
],
alignment: 'left',
spaceBefore: 0,
spaceAfter: 6,
spaceAfter: 2,
};
let nextBlocks = [...doc.blocks];
@@ -725,6 +682,7 @@ export const CreatePDFModal: React.FC<CreatePDFModalProps> = ({
doc={doc}
selectedBlockId={selectedBlockId}
activeRun={activeRun}
activeParagraph={activeParagraph}
activeTool={activeTool}
drawColor={drawColor}
drawWidth={drawWidth}
@@ -109,13 +109,11 @@ const EditableParagraphBlock: React.FC<{
return (
<div
onClick={() => onSelectBlock(block.id, firstRun.id)}
className={`relative rounded px-2.5 py-1.5 transition-all ${
isSelected ? 'bg-blue-50/40 ring-1 ring-brand-primary/50' : 'hover:bg-slate-50'
}`}
className="relative py-0.5"
style={{
textAlign: block.alignment || 'left',
marginTop: `${block.spaceBefore || 0}px`,
marginBottom: `${block.spaceAfter || 4}px`,
marginBottom: `${block.spaceAfter !== undefined ? block.spaceAfter : 2}px`,
}}
>
<div
@@ -127,8 +125,8 @@ const EditableParagraphBlock: React.FC<{
onInput={(e) => onUpdateParagraphText(block.id, e.currentTarget.innerText)}
onBlur={(e) => onUpdateParagraphText(block.id, e.currentTarget.innerText)}
onKeyDown={handleKeyDown}
className="outline-none min-h-[1.5em] empty:before:content-[attr(data-placeholder)] empty:before:text-slate-300 empty:before:pointer-events-none"
data-placeholder="Start typing paragraph text..."
className="outline-none min-h-[1.5em]"
data-placeholder=""
style={{
fontFamily: fontStack,
fontSize: `${firstRun.fontSize || 12}pt`,
@@ -140,26 +138,13 @@ const EditableParagraphBlock: React.FC<{
].filter(Boolean).join(' ') || 'none',
color: firstRun.color || '#0f172a',
backgroundColor: firstRun.highlightColor || 'transparent',
lineHeight: block.lineSpacing || 1.25,
lineHeight: block.lineSpacing ? (block.lineSpacing >= 1.0 ? block.lineSpacing : 1.15 + block.lineSpacing) : 1.15,
letterSpacing: firstRun.letterSpacing !== undefined ? `${firstRun.letterSpacing}px` : (block.letterSpacing !== undefined ? `${block.letterSpacing}px` : 'normal'),
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'pre-wrap',
}}
/>
{isSelected && (
<button
type="button"
title="Delete Paragraph"
onClick={(e) => {
e.stopPropagation();
onRemoveBlock(block.id);
}}
className="absolute -top-2.5 -right-2.5 z-20 flex h-5.5 w-5.5 items-center justify-center rounded-full bg-white text-red-500 hover:bg-red-50 hover:text-red-700 border border-slate-300 shadow-sm text-[11px] font-extrabold cursor-pointer transition-transform hover:scale-110"
>
</button>
)}
</div>
);
};
@@ -237,8 +222,7 @@ const EditableImageBlock: React.FC<{
{/* Floating Quick Action Toolbar */}
{isSelected && (
<div
className={`absolute -top-10 flex items-center gap-1 rounded-md bg-slate-900 px-2 py-1 text-white shadow-xl z-30 text-[11px] whitespace-nowrap max-w-none ${
block.alignment === 'right'
className={`absolute -top-10 flex items-center gap-1 rounded-md bg-slate-900 px-2 py-1 text-white shadow-xl z-30 text-[11px] whitespace-nowrap max-w-none ${block.alignment === 'right'
? 'right-0'
: block.alignment === 'left'
? 'left-0'
@@ -373,8 +357,7 @@ const EditableTableCellDiv: React.FC<{
onFocus={onFocus}
onInput={(e) => onUpdateText(e.currentTarget.innerText)}
onBlur={(e) => onUpdateText(e.currentTarget.innerText)}
className={`outline-none min-h-[1.5em] empty:before:content-[attr(data-placeholder)] empty:before:text-slate-300 empty:before:pointer-events-none ${
isHeader ? 'font-bold' : ''
className={`outline-none min-h-[1.5em] empty:before:content-[attr(data-placeholder)] empty:before:text-slate-300 empty:before:pointer-events-none ${isHeader ? 'font-bold' : ''
}`}
style={{ textAlign: alignment }}
data-placeholder="Cell..."
@@ -439,8 +422,7 @@ const EditableTableBlock: React.FC<{
className={`my-3 flex w-full ${tableAlignClass}`}
>
<div
className={`relative rounded p-1 transition-all inline-block max-w-full ${
isSelected ? 'ring-2 ring-brand-primary bg-blue-50/20' : 'hover:ring-1 hover:ring-slate-300'
className={`relative rounded p-1 transition-all inline-block max-w-full ${isSelected ? 'ring-2 ring-brand-primary bg-blue-50/20' : 'hover:ring-1 hover:ring-slate-300'
}`}
>
{/* Advanced Floating Table Action Bar */}
@@ -455,8 +437,7 @@ const EditableTableBlock: React.FC<{
e.stopPropagation();
onUpdateTableBlock?.(block.id, { hasHeaderRow: !block.hasHeaderRow });
}}
className={`rounded px-2 py-0.5 border ${
block.hasHeaderRow ? 'bg-brand-primary border-brand-primary font-semibold' : 'bg-slate-800 border-slate-700 hover:bg-slate-700'
className={`rounded px-2 py-0.5 border ${block.hasHeaderRow ? 'bg-brand-primary border-brand-primary font-semibold' : 'bg-slate-800 border-slate-700 hover:bg-slate-700'
}`}
>
Header Row
@@ -469,8 +450,7 @@ const EditableTableBlock: React.FC<{
e.stopPropagation();
onUpdateTableBlock?.(block.id, { stripedRows: !block.stripedRows });
}}
className={`rounded px-2 py-0.5 border ${
block.stripedRows ? 'bg-brand-primary border-brand-primary font-semibold' : 'bg-slate-800 border-slate-700 hover:bg-slate-700'
className={`rounded px-2 py-0.5 border ${block.stripedRows ? 'bg-brand-primary border-brand-primary font-semibold' : 'bg-slate-800 border-slate-700 hover:bg-slate-700'
}`}
>
Striped Rows
@@ -637,8 +617,7 @@ const EditableTableBlock: React.FC<{
)}
<table
className={`w-full border-collapse bg-white ${
themeName === 'minimal'
className={`w-full border-collapse bg-white ${themeName === 'minimal'
? 'border-b border-slate-200'
: themeName === 'bordered'
? 'border-2 border-slate-400'
@@ -683,8 +662,7 @@ const EditableTableBlock: React.FC<{
<td
key={cell.id}
onClick={() => setActiveCell({ r: rIdx, c: cIdx })}
className={`p-2.5 text-[12px] relative transition-colors ${
themeName === 'minimal'
className={`p-2.5 text-[12px] relative transition-colors ${themeName === 'minimal'
? 'border-b border-slate-200'
: themeName === 'bordered'
? 'border border-slate-400'
@@ -811,8 +789,7 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
key={`page-${page.pageNumber}`}
onMouseDown={(e) => handleMouseDownPage(e, pageIdx)}
onMouseMove={(e) => handleMouseMovePage(e, pageIdx)}
className={`shrink-0 relative bg-white shadow-xl transition-shadow border border-slate-200 flex flex-col overflow-hidden ${
isDrawTool ? 'cursor-crosshair' : ''
className={`shrink-0 relative bg-white shadow-xl transition-shadow border border-slate-200 flex flex-col overflow-hidden ${isDrawTool ? 'cursor-crosshair' : ''
}`}
style={{
width: `${pageW}px`,
@@ -8,6 +8,7 @@ interface DocumentToolbarProps {
doc: DocumentModel;
selectedBlockId: string | null;
activeRun: TextRun | null;
activeParagraph?: ParagraphBlock | null;
activeTool?: string;
drawColor?: string;
drawWidth?: number;
@@ -38,6 +39,7 @@ const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 64, 72
export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
selectedBlockId,
activeRun,
activeParagraph,
activeTool = 'select',
drawColor = '#2563eb',
drawWidth = 3,
@@ -86,8 +88,7 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
key={c}
type="button"
onClick={() => onUpdateDrawColor?.(c)}
className={`h-5 w-5 rounded-full border border-white shadow-2xs transition-transform ${
drawColor === c ? 'scale-125 ring-2 ring-blue-600' : 'hover:scale-110'
className={`h-5 w-5 rounded-full border border-white shadow-2xs transition-transform ${drawColor === c ? 'scale-125 ring-2 ring-blue-600' : 'hover:scale-110'
}`}
style={{ backgroundColor: c }}
/>
@@ -135,8 +136,7 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
onSelectStamp?.(s.label);
onInsertStamp?.(s.label);
}}
className={`shrink-0 rounded-[6px] border px-2.5 py-1 text-[10.5px] font-bold tracking-wide transition-all hover:scale-105 active:scale-95 shadow-2xs cursor-pointer ${
activeStamp === s.label ? 'ring-2 ring-emerald-600 ring-offset-1' : ''
className={`shrink-0 rounded-[6px] border px-2.5 py-1 text-[10.5px] font-bold tracking-wide transition-all hover:scale-105 active:scale-95 shadow-2xs cursor-pointer ${activeStamp === s.label ? 'ring-2 ring-emerald-600 ring-offset-1' : ''
}`}
style={{ color: s.textColor, borderColor: s.borderColor, background: s.backgroundColor }}
title={`Click to insert ${s.label} stamp`}
@@ -205,13 +205,55 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
<div className="h-4 w-[1px] bg-border-primary mx-0.5" />
{/* Line Gap / Line Spacing */}
<div className="flex items-center gap-1" title="Line Spacing / Line Gap">
<span className="text-[11.5px] font-semibold text-text-secondary"> Line Gap:</span>
<select
className="h-8 rounded-md border border-border-primary bg-bg-primary px-1.5 font-medium text-text-primary outline-none hover:bg-bg-tertiary focus:border-brand-primary text-[12px]"
value={activeParagraph?.lineSpacing || 1.25}
onChange={(e) => onUpdateParagraphFormatting({ lineSpacing: Number(e.target.value) })}
>
<option value={1.0}>1.0 (Single)</option>
<option value={1.15}>1.15 (Compact)</option>
<option value={1.25}>1.25 (Default)</option>
<option value={1.5}>1.5 (1.5 lines)</option>
<option value={2.0}>2.0 (Double)</option>
<option value={2.5}>2.5 (Wide)</option>
<option value={3.0}>3.0 (Extra Wide)</option>
</select>
</div>
{/* Letter Space / Letter Spacing */}
<div className="flex items-center gap-1" title="Letter Spacing / Character Gap">
<span className="text-[11.5px] font-semibold text-text-secondary"> Letter Space:</span>
<select
className="h-8 rounded-md border border-border-primary bg-bg-primary px-1.5 font-medium text-text-primary outline-none hover:bg-bg-tertiary focus:border-brand-primary text-[12px]"
value={activeRun?.letterSpacing ?? (activeParagraph?.letterSpacing ?? 0)}
onChange={(e) => {
const val = Number(e.target.value);
onUpdateRunFormatting({ letterSpacing: val });
onUpdateParagraphFormatting({ letterSpacing: val });
}}
>
<option value={-1}>-1px (Tight)</option>
<option value={0}>0px (Default)</option>
<option value={0.5}>0.5px (Slight)</option>
<option value={1}>1px (Medium)</option>
<option value={1.5}>1.5px (Expanded)</option>
<option value={2}>2px (Wide)</option>
<option value={3}>3px (Very Wide)</option>
<option value={5}>5px (Ultra Wide)</option>
</select>
</div>
<div className="h-4 w-[1px] bg-border-primary mx-0.5" />
{/* Bold / Italic / Underline / Strikethrough */}
<button
type="button"
title="Bold (Ctrl+B)"
onClick={() => onUpdateRunFormatting({ bold: !activeRun?.bold })}
className={`flex h-8 w-8 items-center justify-center rounded-md font-bold transition-colors ${
activeRun?.bold ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
className={`flex h-8 w-8 items-center justify-center rounded-md font-bold transition-colors ${activeRun?.bold ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
B
@@ -221,8 +263,7 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
type="button"
title="Italic (Ctrl+I)"
onClick={() => onUpdateRunFormatting({ italic: !activeRun?.italic })}
className={`flex h-8 w-8 items-center justify-center rounded-md italic font-serif transition-colors ${
activeRun?.italic ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
className={`flex h-8 w-8 items-center justify-center rounded-md italic font-serif transition-colors ${activeRun?.italic ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
I
@@ -232,8 +273,7 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
type="button"
title="Underline (Ctrl+U)"
onClick={() => onUpdateRunFormatting({ underline: !activeRun?.underline })}
className={`flex h-8 w-8 items-center justify-center rounded-md underline transition-colors ${
activeRun?.underline ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
className={`flex h-8 w-8 items-center justify-center rounded-md underline transition-colors ${activeRun?.underline ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
U
@@ -243,8 +283,7 @@ export const DocumentToolbar: React.FC<DocumentToolbarProps> = ({
type="button"
title="Strikethrough"
onClick={() => onUpdateRunFormatting({ strikethrough: !activeRun?.strikethrough })}
className={`flex h-8 w-8 items-center justify-center rounded-md line-through transition-colors ${
activeRun?.strikethrough ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
className={`flex h-8 w-8 items-center justify-center rounded-md line-through transition-colors ${activeRun?.strikethrough ? 'bg-brand-primary text-white' : 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
}`}
>
S
@@ -73,7 +73,8 @@ export class PaginationEngine {
const fontSizePt = block.runs[0]?.fontSize || 12;
const fontSizePx = fontSizePt * PT_TO_PX;
const lineSpacing = block.lineSpacing || 1.25;
const avgCharWidthPx = fontSizePx * 0.52;
const letterSpacingPx = ((block.runs[0]?.letterSpacing !== undefined ? block.runs[0].letterSpacing : block.letterSpacing) || 0) * PT_TO_PX;
const avgCharWidthPx = fontSizePx * 0.52 + letterSpacingPx;
const charsPerLine = Math.max(1, Math.floor(usableWidthPx / avgCharWidthPx));
const paragraphs = fullText.split('\n');
@@ -88,7 +89,7 @@ export class PaginationEngine {
const paddingPx = 4;
const spaceBeforePx = (block.spaceBefore || 0) * PT_TO_PX;
const spaceAfterPx = (block.spaceAfter || 4) * PT_TO_PX;
const spaceAfterPx = (block.spaceAfter !== undefined ? block.spaceAfter : 2) * PT_TO_PX;
const flexGapPx = 4;
const contentHeight = totalLines * (fontSizePx * lineSpacing) + paddingPx + spaceBeforePx + spaceAfterPx + flexGapPx;
@@ -127,7 +127,12 @@ export class PdfDocumentRenderer {
indentOffset = 18;
}
const lineSpacing = pBlock.lineSpacing || 1.15;
const rawLineSpacing = pBlock.lineSpacing || 0;
const lineSpacing = rawLineSpacing >= 1.0 ? rawLineSpacing : 1.15 + rawLineSpacing;
const letterSpacing = (pBlock.runs[0]?.letterSpacing !== undefined ? pBlock.runs[0].letterSpacing : pBlock.letterSpacing) || 0;
if (typeof (pdf as any).setCharSpace === 'function') {
(pdf as any).setCharSpace(letterSpacing * 0.75);
}
const lines = pdf.splitTextToSize(textToRender, usableWidth - indentOffset);
const blockHeight = lines.length * (fontSize * lineSpacing);
@@ -150,6 +155,9 @@ export class PdfDocumentRenderer {
pdf.text(line, xPos, currentY + fontSize, { align: alignOption });
currentY += fontSize * lineSpacing;
});
if (typeof (pdf as any).setCharSpace === 'function') {
(pdf as any).setCharSpace(0);
}
// Hyperlinks handling
pBlock.runs.forEach((run) => {
@@ -10,6 +10,7 @@ export interface TextRun {
color?: string;
highlightColor?: string;
linkUrl?: string;
letterSpacing?: number;
}
export type BlockType = 'paragraph' | 'heading' | 'list-item' | 'table' | 'image' | 'quote' | 'page-break';
@@ -21,7 +22,8 @@ export interface ParagraphBlock {
headingLevel?: 1 | 2 | 3 | 4;
runs: TextRun[];
alignment?: 'left' | 'center' | 'right' | 'justify';
lineSpacing?: number; // 1.0, 1.15, 1.5, 2.0
lineSpacing?: number; // 1.0, 1.15, 1.25, 1.5, 2.0, 2.5, 3.0
letterSpacing?: number; // -1, 0, 0.5, 1.0, 1.5, 2.0, 3.0, 5.0
spaceBefore?: number;
spaceAfter?: number;
indentLeft?: number;
@@ -125,7 +127,7 @@ export const DEFAULT_STYLES: Record<string, StyleDefinition> = {
color: '#0f172a',
alignment: 'left',
spaceBefore: 0,
spaceAfter: 6,
spaceAfter: 2,
},
Title: {
name: 'Title',
+1 -3
View File
@@ -727,9 +727,7 @@ class GatewayService {
method: 'POST',
});
if (!response.ok) {
const errJson = await response.json().catch(() => null);
const detail = errJson?.detail || response.statusText;
throw new Error(detail);
throw new Error(`OCR failed: ${response.statusText}`);
}
return response.json();
}