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:
@@ -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/
|
||||
|
||||
+5
-15
@@ -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 {
|
||||
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));
|
||||
}
|
||||
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', '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);
|
||||
}
|
||||
|
||||
@@ -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,13 +222,12 @@ 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'
|
||||
: 'left-1/2 -translate-x-1/2'
|
||||
}`}
|
||||
? 'left-0'
|
||||
: 'left-1/2 -translate-x-1/2'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -373,9 +357,8 @@ 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..."
|
||||
/>
|
||||
@@ -412,305 +395,300 @@ const EditableTableBlock: React.FC<{
|
||||
onUpdateTableBlock,
|
||||
onUpdateTableCellAlignment,
|
||||
}) => {
|
||||
const [activeCell, setActiveCell] = useState<{ r: number; c: number } | null>(null);
|
||||
const [activeCell, setActiveCell] = useState<{ r: number; c: number } | null>(null);
|
||||
|
||||
const colors = [
|
||||
{ label: 'Clear', value: 'transparent' },
|
||||
{ label: 'Gray', value: '#f1f5f9' },
|
||||
{ label: 'Blue', value: '#dbeafe' },
|
||||
{ label: 'Green', value: '#dcfce7' },
|
||||
{ label: 'Yellow', value: '#fef9c3' },
|
||||
{ label: 'Purple', value: '#f3e8ff' },
|
||||
{ label: 'Red', value: '#fee2e2' },
|
||||
];
|
||||
const colors = [
|
||||
{ label: 'Clear', value: 'transparent' },
|
||||
{ label: 'Gray', value: '#f1f5f9' },
|
||||
{ label: 'Blue', value: '#dbeafe' },
|
||||
{ label: 'Green', value: '#dcfce7' },
|
||||
{ label: 'Yellow', value: '#fef9c3' },
|
||||
{ label: 'Purple', value: '#f3e8ff' },
|
||||
{ label: 'Red', value: '#fee2e2' },
|
||||
];
|
||||
|
||||
const tableAlignClass =
|
||||
block.alignment === 'right'
|
||||
? 'justify-end'
|
||||
: block.alignment === 'center'
|
||||
? 'justify-center'
|
||||
: 'justify-start';
|
||||
const tableAlignClass =
|
||||
block.alignment === 'right'
|
||||
? 'justify-end'
|
||||
: block.alignment === 'center'
|
||||
? 'justify-center'
|
||||
: 'justify-start';
|
||||
|
||||
const themeName = block.themeName || 'default';
|
||||
const themeName = block.themeName || 'default';
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelectBlock(block.id)}
|
||||
className={`my-3 flex w-full ${tableAlignClass}`}
|
||||
>
|
||||
return (
|
||||
<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'
|
||||
}`}
|
||||
onClick={() => onSelectBlock(block.id)}
|
||||
className={`my-3 flex w-full ${tableAlignClass}`}
|
||||
>
|
||||
{/* Advanced Floating Table Action Bar */}
|
||||
{isSelected && (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-1.5 rounded-md bg-slate-900 px-3 py-1.5 text-white shadow-lg text-[11px] z-20 select-none">
|
||||
<span className="font-semibold text-slate-300 mr-1">Table:</span>
|
||||
|
||||
{/* Header Row Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Header Row
|
||||
</button>
|
||||
|
||||
{/* Striped Rows Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Striped Rows
|
||||
</button>
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Theme Preset Selector */}
|
||||
<span className="text-slate-400">Theme:</span>
|
||||
<select
|
||||
value={themeName}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableBlock?.(block.id, { themeName: e.target.value as any });
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-slate-800 text-white rounded px-1.5 py-0.5 outline-none border border-slate-700 text-[11px]"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="modern">Modern Navy</option>
|
||||
<option value="minimal">Minimalist</option>
|
||||
<option value="bordered">Grid Bordered</option>
|
||||
</select>
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Row / Col Manipulations */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableRow?.(block.id, activeCell?.r ?? 0, 'above');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Row Above
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableRow?.(block.id, activeCell?.r ?? block.rows.length - 1, 'below');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Row Below
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableColumn?.(block.id, activeCell?.c ?? 0, 'left');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Col Left
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableColumn?.(block.id, activeCell?.c ?? (block.rows[0]?.cells.length || 1) - 1, 'right');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Col Right
|
||||
</button>
|
||||
|
||||
{activeCell && (
|
||||
<>
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Cell Alignment */}
|
||||
<span className="text-slate-400">Cell Align:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'left');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Left"
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'center');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Center"
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'right');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Right"
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTableRow?.(block.id, activeCell.r);
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-red-900/80 hover:bg-red-800 text-red-200 ml-1"
|
||||
>
|
||||
- Row
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTableColumn?.(block.id, activeCell.c);
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-red-900/80 hover:bg-red-800 text-red-200"
|
||||
>
|
||||
- Col
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
<span className="text-slate-400">Fill:</span>
|
||||
{colors.map((c) => (
|
||||
<button
|
||||
key={c.label}
|
||||
type="button"
|
||||
title={`Fill Cell ${c.label}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeCell) {
|
||||
onUpdateTableCellBackground?.(block.id, activeCell.r, activeCell.c, c.value);
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 rounded border border-slate-600 hover:scale-110 transition-transform"
|
||||
style={{ backgroundColor: c.value === 'transparent' ? '#ffffff' : c.value }}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveBlock(block.id);
|
||||
}}
|
||||
className="rounded px-2 py-0.5 bg-red-600 hover:bg-red-700 text-white font-medium"
|
||||
>
|
||||
Delete Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table
|
||||
className={`w-full border-collapse bg-white ${
|
||||
themeName === 'minimal'
|
||||
? 'border-b border-slate-200'
|
||||
: themeName === 'bordered'
|
||||
? 'border-2 border-slate-400'
|
||||
: 'border border-slate-300'
|
||||
}`}
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<tbody>
|
||||
{block.rows.map((row, rIdx) => {
|
||||
const isHeaderRow = (block.hasHeaderRow && rIdx === 0) || row.isHeader;
|
||||
const isStriped = block.stripedRows && rIdx % 2 === 1;
|
||||
{/* Advanced Floating Table Action Bar */}
|
||||
{isSelected && (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-1.5 rounded-md bg-slate-900 px-3 py-1.5 text-white shadow-lg text-[11px] z-20 select-none">
|
||||
<span className="font-semibold text-slate-300 mr-1">Table:</span>
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={
|
||||
isHeaderRow
|
||||
? themeName === 'modern'
|
||||
? 'bg-slate-900 text-white'
|
||||
: 'bg-slate-100 font-bold'
|
||||
: isStriped
|
||||
? 'bg-slate-50/80'
|
||||
: ''
|
||||
}
|
||||
{/* Header Row Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Header Row
|
||||
</button>
|
||||
|
||||
{/* Striped Rows Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
Striped Rows
|
||||
</button>
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Theme Preset Selector */}
|
||||
<span className="text-slate-400">Theme:</span>
|
||||
<select
|
||||
value={themeName}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableBlock?.(block.id, { themeName: e.target.value as any });
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-slate-800 text-white rounded px-1.5 py-0.5 outline-none border border-slate-700 text-[11px]"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="modern">Modern Navy</option>
|
||||
<option value="minimal">Minimalist</option>
|
||||
<option value="bordered">Grid Bordered</option>
|
||||
</select>
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Row / Col Manipulations */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableRow?.(block.id, activeCell?.r ?? 0, 'above');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Row Above
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableRow?.(block.id, activeCell?.r ?? block.rows.length - 1, 'below');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Row Below
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableColumn?.(block.id, activeCell?.c ?? 0, 'left');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Col Left
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddTableColumn?.(block.id, activeCell?.c ?? (block.rows[0]?.cells.length || 1) - 1, 'right');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
>
|
||||
+ Col Right
|
||||
</button>
|
||||
|
||||
{activeCell && (
|
||||
<>
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
{/* Cell Alignment */}
|
||||
<span className="text-slate-400">Cell Align:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'left');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Left"
|
||||
>
|
||||
Left
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'center');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Center"
|
||||
>
|
||||
Center
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpdateTableCellAlignment?.(block.id, activeCell.r, activeCell.c, 'right');
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-slate-800 hover:bg-slate-700"
|
||||
title="Align Cell Right"
|
||||
>
|
||||
Right
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTableRow?.(block.id, activeCell.r);
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-red-900/80 hover:bg-red-800 text-red-200 ml-1"
|
||||
>
|
||||
- Row
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteTableColumn?.(block.id, activeCell.c);
|
||||
}}
|
||||
className="rounded px-1.5 py-0.5 bg-red-900/80 hover:bg-red-800 text-red-200"
|
||||
>
|
||||
- Col
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-3 w-[1px] bg-slate-700 my-auto" />
|
||||
|
||||
<span className="text-slate-400">Fill:</span>
|
||||
{colors.map((c) => (
|
||||
<button
|
||||
key={c.label}
|
||||
type="button"
|
||||
title={`Fill Cell ${c.label}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (activeCell) {
|
||||
onUpdateTableCellBackground?.(block.id, activeCell.r, activeCell.c, c.value);
|
||||
}
|
||||
}}
|
||||
className="h-4 w-4 rounded border border-slate-600 hover:scale-110 transition-transform"
|
||||
style={{ backgroundColor: c.value === 'transparent' ? '#ffffff' : c.value }}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveBlock(block.id);
|
||||
}}
|
||||
className="rounded px-2 py-0.5 bg-red-600 hover:bg-red-700 text-white font-medium"
|
||||
>
|
||||
{row.cells.map((cell, cIdx) => {
|
||||
const cellText = cell.runs.map((r) => r.text).join('');
|
||||
const isCellFocused = activeCell?.r === rIdx && activeCell?.c === cIdx;
|
||||
Delete Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
const cellBg =
|
||||
cell.backgroundColor ||
|
||||
(isHeaderRow
|
||||
<table
|
||||
className={`w-full border-collapse bg-white ${themeName === 'minimal'
|
||||
? 'border-b border-slate-200'
|
||||
: themeName === 'bordered'
|
||||
? 'border-2 border-slate-400'
|
||||
: 'border border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<tbody>
|
||||
{block.rows.map((row, rIdx) => {
|
||||
const isHeaderRow = (block.hasHeaderRow && rIdx === 0) || row.isHeader;
|
||||
const isStriped = block.stripedRows && rIdx % 2 === 1;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={
|
||||
isHeaderRow
|
||||
? themeName === 'modern'
|
||||
? '#0f172a'
|
||||
: '#f1f5f9'
|
||||
? 'bg-slate-900 text-white'
|
||||
: 'bg-slate-100 font-bold'
|
||||
: isStriped
|
||||
? '#f8fafc'
|
||||
: 'transparent');
|
||||
? 'bg-slate-50/80'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{row.cells.map((cell, cIdx) => {
|
||||
const cellText = cell.runs.map((r) => r.text).join('');
|
||||
const isCellFocused = activeCell?.r === rIdx && activeCell?.c === cIdx;
|
||||
|
||||
const cellTextColor = isHeaderRow && themeName === 'modern' ? '#ffffff' : '#0f172a';
|
||||
const cellBg =
|
||||
cell.backgroundColor ||
|
||||
(isHeaderRow
|
||||
? themeName === 'modern'
|
||||
? '#0f172a'
|
||||
: '#f1f5f9'
|
||||
: isStriped
|
||||
? '#f8fafc'
|
||||
: 'transparent');
|
||||
|
||||
return (
|
||||
<td
|
||||
key={cell.id}
|
||||
onClick={() => setActiveCell({ r: rIdx, c: cIdx })}
|
||||
className={`p-2.5 text-[12px] relative transition-colors ${
|
||||
themeName === 'minimal'
|
||||
? 'border-b border-slate-200'
|
||||
: themeName === 'bordered'
|
||||
? 'border border-slate-400'
|
||||
: 'border border-slate-300'
|
||||
} ${isCellFocused ? 'ring-2 ring-brand-primary z-10' : ''}`}
|
||||
style={{ backgroundColor: cellBg, color: cellTextColor }}
|
||||
>
|
||||
<EditableTableCellDiv
|
||||
cellText={cellText}
|
||||
alignment={cell.alignment}
|
||||
isHeader={isHeaderRow}
|
||||
onUpdateText={(text) => onUpdateTableCell?.(block.id, rIdx, cIdx, text)}
|
||||
onFocus={() => setActiveCell({ r: rIdx, c: cIdx })}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
const cellTextColor = isHeaderRow && themeName === 'modern' ? '#ffffff' : '#0f172a';
|
||||
|
||||
return (
|
||||
<td
|
||||
key={cell.id}
|
||||
onClick={() => setActiveCell({ r: rIdx, c: cIdx })}
|
||||
className={`p-2.5 text-[12px] relative transition-colors ${themeName === 'minimal'
|
||||
? 'border-b border-slate-200'
|
||||
: themeName === 'bordered'
|
||||
? 'border border-slate-400'
|
||||
: 'border border-slate-300'
|
||||
} ${isCellFocused ? 'ring-2 ring-brand-primary z-10' : ''}`}
|
||||
style={{ backgroundColor: cellBg, color: cellTextColor }}
|
||||
>
|
||||
<EditableTableCellDiv
|
||||
cellText={cellText}
|
||||
alignment={cell.alignment}
|
||||
isHeader={isHeaderRow}
|
||||
onUpdateText={(text) => onUpdateTableCell?.(block.id, rIdx, cIdx, text)}
|
||||
onFocus={() => setActiveCell({ r: rIdx, c: cIdx })}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
||||
doc,
|
||||
@@ -811,9 +789,8 @@ 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`,
|
||||
height: `${pageH}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,9 +88,8 @@ 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,9 +136,8 @@ 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,14 +205,56 @@ 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
|
||||
</button>
|
||||
@@ -221,9 +263,8 @@ 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
|
||||
</button>
|
||||
@@ -232,9 +273,8 @@ 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
|
||||
</button>
|
||||
@@ -243,9 +283,8 @@ 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
|
||||
</button>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user