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