mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge branch 'codex/add-pdf-to-json-and-json-to-pdf-features' into demo
This commit is contained in:
commit
c13b32fab3
@ -2647,8 +2647,18 @@ public class PdfJsonConversionService {
|
|||||||
pageNumber);
|
pageNumber);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
contentStream.showText(
|
try {
|
||||||
new String(encoded, StandardCharsets.ISO_8859_1));
|
contentStream.showText(
|
||||||
|
new String(encoded, StandardCharsets.ISO_8859_1));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to render text '{}' with font {} on page {}: {}",
|
||||||
|
run.text(),
|
||||||
|
run.font().getName(),
|
||||||
|
pageNumber,
|
||||||
|
ex.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3380,8 +3390,18 @@ public class PdfJsonConversionService {
|
|||||||
// or return null to trigger fallback font
|
// or return null to trigger fallback font
|
||||||
} else if (!isType3Font || fontModel == null) {
|
} else if (!isType3Font || fontModel == null) {
|
||||||
// For non-Type3 fonts without Type3 metadata, use standard encoding
|
// For non-Type3 fonts without Type3 metadata, use standard encoding
|
||||||
byte[] encoded = font.encode(text);
|
try {
|
||||||
return sanitizeEncoded(encoded);
|
byte[] encoded = font.encode(text);
|
||||||
|
return sanitizeEncoded(encoded);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.info(
|
||||||
|
"[FONT-DEBUG] Font {} cannot encode text '{}': {}",
|
||||||
|
font.getName(),
|
||||||
|
text,
|
||||||
|
ex.getMessage());
|
||||||
|
// Return null to trigger fallback font mechanism
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type3 glyph mapping logic (for actual Type3 fonts AND normalized Type3 fonts)
|
// Type3 glyph mapping logic (for actual Type3 fonts AND normalized Type3 fonts)
|
||||||
|
|||||||
@ -4471,7 +4471,8 @@
|
|||||||
"title": "PDF Editor",
|
"title": "PDF Editor",
|
||||||
"badges": {
|
"badges": {
|
||||||
"unsaved": "Unsaved changes",
|
"unsaved": "Unsaved changes",
|
||||||
"modified": "Edited"
|
"modified": "Edited",
|
||||||
|
"earlyAccess": "Early Access - Pre-Alpha"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
"title": "Document Controls"
|
"title": "Document Controls"
|
||||||
@ -4526,6 +4527,12 @@
|
|||||||
"paragraph": "Paragraph",
|
"paragraph": "Paragraph",
|
||||||
"singleLine": "Single Line"
|
"singleLine": "Single Line"
|
||||||
},
|
},
|
||||||
|
"modeChange": {
|
||||||
|
"title": "Confirm Mode Change",
|
||||||
|
"warning": "Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"confirm": "Reset and Change Mode"
|
||||||
|
},
|
||||||
"disclaimer": {
|
"disclaimer": {
|
||||||
"heading": "Preview limitations",
|
"heading": "Preview limitations",
|
||||||
"textFocus": "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.",
|
"textFocus": "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.",
|
||||||
|
|||||||
@ -255,8 +255,8 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
|
|||||||
|
|
||||||
{/* Font List */}
|
{/* Font List */}
|
||||||
<Stack gap={4} mt="xs">
|
<Stack gap={4} mt="xs">
|
||||||
{fonts.map((font) => (
|
{fonts.map((font, index) => (
|
||||||
<FontDetailItem key={font.fontId} analysis={font} />
|
<FontDetailItem key={`${font.fontId}-${index}`} analysis={font} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@ -318,6 +318,9 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
const [activeGroupId, setActiveGroupId] = useState<string | null>(null);
|
||||||
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
|
||||||
const [activeImageId, setActiveImageId] = useState<string | null>(null);
|
const [activeImageId, setActiveImageId] = useState<string | null>(null);
|
||||||
|
const draggingImageRef = useRef<string | null>(null);
|
||||||
|
const rndRefs = useRef<Map<string, any>>(new Map());
|
||||||
|
const pendingDragUpdateRef = useRef<number | null>(null);
|
||||||
const [fontFamilies, setFontFamilies] = useState<Map<string, string>>(new Map());
|
const [fontFamilies, setFontFamilies] = useState<Map<string, string>>(new Map());
|
||||||
const [autoScaleText, setAutoScaleText] = useState(true);
|
const [autoScaleText, setAutoScaleText] = useState(true);
|
||||||
const [textScales, setTextScales] = useState<Map<string, number>>(new Map());
|
const [textScales, setTextScales] = useState<Map<string, number>>(new Map());
|
||||||
@ -930,6 +933,41 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
}
|
}
|
||||||
}, [editingGroupId]);
|
}, [editingGroupId]);
|
||||||
|
|
||||||
|
// Sync image positions when not dragging (handles stutters/re-renders)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const isDragging = draggingImageRef.current !== null;
|
||||||
|
if (isDragging) {
|
||||||
|
return; // Don't sync during drag
|
||||||
|
}
|
||||||
|
|
||||||
|
pageImages.forEach((image) => {
|
||||||
|
if (!image?.id) return;
|
||||||
|
|
||||||
|
const imageId = image.id;
|
||||||
|
const rndRef = rndRefs.current.get(imageId);
|
||||||
|
if (!rndRef || !rndRef.updatePosition) return;
|
||||||
|
|
||||||
|
const bounds = getImageBounds(image);
|
||||||
|
const width = Math.max(bounds.right - bounds.left, 1);
|
||||||
|
const height = Math.max(bounds.top - bounds.bottom, 1);
|
||||||
|
const cssLeft = bounds.left * scale;
|
||||||
|
const cssTop = (pageHeight - bounds.top) * scale;
|
||||||
|
|
||||||
|
// Get current position from Rnd component
|
||||||
|
const currentState = rndRef.state || {};
|
||||||
|
const currentX = currentState.x ?? 0;
|
||||||
|
const currentY = currentState.y ?? 0;
|
||||||
|
|
||||||
|
// Calculate drift
|
||||||
|
const drift = Math.abs(currentX - cssLeft) + Math.abs(currentY - cssTop);
|
||||||
|
|
||||||
|
// Only sync if drift is significant (more than 3px)
|
||||||
|
if (drift > 3) {
|
||||||
|
rndRef.updatePosition({ x: cssLeft, y: cssTop });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [pageImages, scale, pageHeight]);
|
||||||
|
|
||||||
const handlePageChange = (pageNumber: number) => {
|
const handlePageChange = (pageNumber: number) => {
|
||||||
setActiveGroupId(null);
|
setActiveGroupId(null);
|
||||||
setEditingGroupId(null);
|
setEditingGroupId(null);
|
||||||
@ -1413,25 +1451,43 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Rnd
|
<Rnd
|
||||||
|
ref={(ref) => {
|
||||||
|
if (ref) {
|
||||||
|
rndRefs.current.set(imageId, ref);
|
||||||
|
} else {
|
||||||
|
rndRefs.current.delete(imageId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
key={`image-${imageId}`}
|
key={`image-${imageId}`}
|
||||||
bounds="parent"
|
bounds="parent"
|
||||||
size={{ width: cssWidth, height: cssHeight }}
|
size={{ width: cssWidth, height: cssHeight }}
|
||||||
position={{ x: cssLeft, y: cssTop }}
|
position={{ x: cssLeft, y: cssTop }}
|
||||||
onDragStart={() => {
|
onDragStart={(_event, data) => {
|
||||||
setActiveGroupId(null);
|
setActiveGroupId(null);
|
||||||
setEditingGroupId(null);
|
setEditingGroupId(null);
|
||||||
setActiveImageId(imageId);
|
setActiveImageId(imageId);
|
||||||
|
draggingImageRef.current = imageId;
|
||||||
}}
|
}}
|
||||||
onDrag={(_event, data) => {
|
onDrag={(_event, data) => {
|
||||||
emitImageTransform(
|
// Cancel any pending update
|
||||||
imageId,
|
if (pendingDragUpdateRef.current) {
|
||||||
data.x,
|
cancelAnimationFrame(pendingDragUpdateRef.current);
|
||||||
data.y,
|
}
|
||||||
cssWidth,
|
|
||||||
cssHeight,
|
// Schedule update on next frame to batch rapid drag events
|
||||||
);
|
pendingDragUpdateRef.current = requestAnimationFrame(() => {
|
||||||
|
const rndRef = rndRefs.current.get(imageId);
|
||||||
|
if (rndRef && rndRef.updatePosition) {
|
||||||
|
rndRef.updatePosition({ x: data.x, y: data.y });
|
||||||
|
}
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onDragStop={(_event, data) => {
|
onDragStop={(_event, data) => {
|
||||||
|
if (pendingDragUpdateRef.current) {
|
||||||
|
cancelAnimationFrame(pendingDragUpdateRef.current);
|
||||||
|
pendingDragUpdateRef.current = null;
|
||||||
|
}
|
||||||
|
draggingImageRef.current = null;
|
||||||
emitImageTransform(
|
emitImageTransform(
|
||||||
imageId,
|
imageId,
|
||||||
data.x,
|
data.x,
|
||||||
@ -1444,19 +1500,10 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
setActiveImageId(imageId);
|
setActiveImageId(imageId);
|
||||||
setActiveGroupId(null);
|
setActiveGroupId(null);
|
||||||
setEditingGroupId(null);
|
setEditingGroupId(null);
|
||||||
}}
|
draggingImageRef.current = imageId;
|
||||||
onResize={(_event, _direction, ref, _delta, position) => {
|
|
||||||
const nextWidth = parseFloat(ref.style.width);
|
|
||||||
const nextHeight = parseFloat(ref.style.height);
|
|
||||||
emitImageTransform(
|
|
||||||
imageId,
|
|
||||||
position.x,
|
|
||||||
position.y,
|
|
||||||
nextWidth,
|
|
||||||
nextHeight,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
onResizeStop={(_event, _direction, ref, _delta, position) => {
|
onResizeStop={(_event, _direction, ref, _delta, position) => {
|
||||||
|
draggingImageRef.current = null;
|
||||||
const nextWidth = parseFloat(ref.style.width);
|
const nextWidth = parseFloat(ref.style.width);
|
||||||
const nextHeight = parseFloat(ref.style.height);
|
const nextHeight = parseFloat(ref.style.height);
|
||||||
emitImageTransform(
|
emitImageTransform(
|
||||||
|
|||||||
@ -976,6 +976,36 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
} catch (textError) {
|
} catch (textError) {
|
||||||
console.warn('[PdfTextEditor] Failed to strip text from preview', textError);
|
console.warn('[PdfTextEditor] Failed to strip text from preview', textError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also mask out images to prevent ghost/shadow images when they're moved
|
||||||
|
try {
|
||||||
|
const pageImages = imagesByPage[pageIndex] ?? [];
|
||||||
|
if (pageImages.length > 0) {
|
||||||
|
context.save();
|
||||||
|
context.globalCompositeOperation = 'destination-out';
|
||||||
|
context.fillStyle = '#000000';
|
||||||
|
for (const image of pageImages) {
|
||||||
|
if (!image) continue;
|
||||||
|
// Get image bounds in PDF coordinates
|
||||||
|
const left = image.left ?? image.x ?? 0;
|
||||||
|
const bottom = image.bottom ?? image.y ?? 0;
|
||||||
|
const width = image.width ?? Math.max((image.right ?? left) - left, 0);
|
||||||
|
const height = image.height ?? Math.max((image.top ?? bottom) - bottom, 0);
|
||||||
|
const right = left + width;
|
||||||
|
const top = bottom + height;
|
||||||
|
|
||||||
|
// Convert to canvas coordinates (PDF origin is bottom-left, canvas is top-left)
|
||||||
|
const canvasX = left * scale;
|
||||||
|
const canvasY = canvas.height - top * scale;
|
||||||
|
const canvasWidth = width * scale;
|
||||||
|
const canvasHeight = height * scale;
|
||||||
|
context.fillRect(canvasX, canvasY, canvasWidth, canvasHeight);
|
||||||
|
}
|
||||||
|
context.restore();
|
||||||
|
}
|
||||||
|
} catch (imageError) {
|
||||||
|
console.warn('[PdfTextEditor] Failed to strip images from preview', imageError);
|
||||||
|
}
|
||||||
const dataUrl = canvas.toDataURL('image/png');
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
page.cleanup();
|
page.cleanup();
|
||||||
if (previewRequestIdRef.current !== currentToken) {
|
if (previewRequestIdRef.current !== currentToken) {
|
||||||
@ -993,7 +1023,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
previewRenderingRef.current.delete(pageIndex);
|
previewRenderingRef.current.delete(pageIndex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hasVectorPreview],
|
[hasVectorPreview, imagesByPage],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-group text when grouping mode changes without forcing a full reload
|
// Re-group text when grouping mode changes without forcing a full reload
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user