content completed
This commit is contained in:
@@ -76,6 +76,7 @@ find_package(nlohmann_json CONFIG REQUIRED)
|
||||
find_package(qpdf CONFIG REQUIRED)
|
||||
if(WIN32 AND DEFINED VCPKG_TARGET_TRIPLET)
|
||||
link_directories("${CMAKE_BINARY_DIR}/vcpkg_installed/${VCPKG_TARGET_TRIPLET}/lib")
|
||||
link_directories("${CMAKE_BINARY_DIR}/vcpkg_installed/${VCPKG_TARGET_TRIPLET}/debug/lib")
|
||||
endif()
|
||||
|
||||
if(PDFENGINE_BUILD_TESTS)
|
||||
|
||||
@@ -101,31 +101,56 @@ public:
|
||||
pdfengine::Lexer lexer(stream->decodedContent);
|
||||
auto tokens = lexer.tokenize();
|
||||
pdfengine::ContentParser parser(tokens);
|
||||
pdfengine::ContentBuilder builder;
|
||||
|
||||
auto objects = builder.build(parser.parse());
|
||||
auto operations = parser.parse();
|
||||
|
||||
int textCount = 0;
|
||||
bool modified = false;
|
||||
for (auto& obj : objects) {
|
||||
if (obj->getType() == pdfengine::ContentObjectType::Text) {
|
||||
if (textCount == object_index) {
|
||||
auto* textObj = static_cast<pdfengine::TextObject*>(obj.get());
|
||||
textObj->text = new_text;
|
||||
modified = true;
|
||||
break;
|
||||
for (auto& op : operations) {
|
||||
if (op.op == "Tj" || op.op == "'") {
|
||||
if (op.operands.empty()) continue;
|
||||
auto& strNode = op.operands.back();
|
||||
if (strNode->type == pdfengine::AstNodeType::String || strNode->type == pdfengine::AstNodeType::HexString) {
|
||||
if (textCount == object_index) {
|
||||
strNode->type = pdfengine::AstNodeType::String;
|
||||
strNode->stringValue = new_text;
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
textCount++;
|
||||
}
|
||||
} else if (op.op == "TJ") {
|
||||
if (op.operands.empty()) continue;
|
||||
auto& arrNode = op.operands.back();
|
||||
if (arrNode->type == pdfengine::AstNodeType::Array) {
|
||||
std::string combinedText;
|
||||
for (const auto& item : arrNode->arrayItems) {
|
||||
if (item->type == pdfengine::AstNodeType::String) {
|
||||
combinedText += item->stringValue;
|
||||
} else if (item->type == pdfengine::AstNodeType::HexString) {
|
||||
combinedText += std::string(item->bytesValue.begin(), item->bytesValue.end());
|
||||
} else if (item->type == pdfengine::AstNodeType::Number) {
|
||||
if (item->numberValue < -500.0) combinedText += " ";
|
||||
}
|
||||
}
|
||||
if (!combinedText.empty()) {
|
||||
if (textCount == object_index) {
|
||||
arrNode->arrayItems.clear();
|
||||
auto newStrNode = std::make_shared<pdfengine::AstNode>(pdfengine::AstNodeType::String);
|
||||
newStrNode->stringValue = new_text;
|
||||
arrNode->arrayItems.push_back(std::move(newStrNode));
|
||||
modified = true;
|
||||
break;
|
||||
}
|
||||
textCount++;
|
||||
}
|
||||
}
|
||||
textCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified) return false;
|
||||
|
||||
pdfengine::ContentSerializer cSerializer;
|
||||
auto newOps = cSerializer.serialize(objects);
|
||||
|
||||
pdfengine::AstSerializer astSerializer;
|
||||
std::string newRawStream = astSerializer.serialize(newOps);
|
||||
std::string newRawStream = astSerializer.serialize(operations);
|
||||
|
||||
pdfengine::qpdf_layer::QpdfWriter writer;
|
||||
auto res = writer.replacePageStreamAndSave(filepath_, dest_path, page_index, newRawStream);
|
||||
|
||||
@@ -318,7 +318,7 @@ export interface TextObjectResponse {
|
||||
}
|
||||
|
||||
class GatewayService {
|
||||
private baseUrl: string;
|
||||
public baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
// In dev environment, FastAPI gateway runs on port 8000 by default.
|
||||
|
||||
@@ -62,22 +62,18 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
setEditingIndex(null);
|
||||
|
||||
if (newText === obj.text) return;
|
||||
|
||||
try {
|
||||
const res = await gatewayService.updateTextObject(documentId, pageIndex, idx, newText);
|
||||
if (res.success) {
|
||||
toast('Stream object updated', 'success');
|
||||
// Update local state optimistically
|
||||
setObjects(prev => {
|
||||
const next = [...prev];
|
||||
next[idx] = { ...next[idx], text: newText };
|
||||
return next;
|
||||
});
|
||||
onEditSuccess();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast('Failed to update text object', 'error');
|
||||
await gatewayService.updateTextObject(documentId, pageIndex, idx, newText);
|
||||
toast('Text updated successfully', 'success');
|
||||
// Update local state optimistically
|
||||
setObjects(prev => {
|
||||
const next = [...prev];
|
||||
next[idx] = { ...next[idx], text: newText };
|
||||
return next;
|
||||
});
|
||||
onEditSuccess();
|
||||
} catch (e: any) {
|
||||
toast(`Failed to update text: ${e.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,12 +81,22 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
const uniqueFonts = Array.from(new Set(objects.map(o => o.fontName).filter(Boolean)));
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 z-[40]" style={{ width: `${width}px`, height: `${height}px` }}>
|
||||
<style>
|
||||
{uniqueFonts.map(fontName => `
|
||||
@font-face {
|
||||
font-family: 'PDF_${fontName}';
|
||||
src: url('${gatewayService.baseUrl}/documents/${documentId}/font?internal_font_id=${encodeURIComponent(fontName)}');
|
||||
}
|
||||
`).join('\n')}
|
||||
</style>
|
||||
{objects.map((obj, i) => {
|
||||
// tm[4] is X, tm[5] is Y (baseline, bottom-left origin)
|
||||
const pdfX = obj.tm[4];
|
||||
@@ -100,7 +106,8 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
const left = pdfX * zoom;
|
||||
// pdfY is baseline. So baseline in screen coords from top:
|
||||
const baselineScreenTop = (heightPts - pdfY) * zoom;
|
||||
const fontSizeScreen = obj.fontSize * zoom;
|
||||
const scaleY = obj.tm ? Math.abs(obj.tm[3]) : 1;
|
||||
const fontSizeScreen = obj.fontSize * scaleY * zoom;
|
||||
|
||||
// Approximate box
|
||||
const top = baselineScreenTop - (fontSizeScreen * 0.8); // 80% ascent
|
||||
@@ -108,6 +115,7 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
const boxWidth = Math.max(obj.text.length * fontSizeScreen * 0.5, 20); // estimate width
|
||||
|
||||
const isEditing = editingIndex === i;
|
||||
const fontFamily = obj.fontName ? `'PDF_${obj.fontName}', sans-serif` : 'sans-serif';
|
||||
|
||||
return (
|
||||
<div key={i}>
|
||||
@@ -124,16 +132,17 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
}}
|
||||
title={`Font: ${obj.fontName}, Size: ${obj.fontSize}`}
|
||||
>
|
||||
{/* Visual debug text, mostly transparent to see what's underneath but know where box is */}
|
||||
<span className="opacity-0">{obj.text}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="absolute z-[41] bg-white shadow-lg border border-blue-500 rounded px-1"
|
||||
className="absolute z-[41] bg-white shadow-lg border border-blue-500 rounded"
|
||||
style={{
|
||||
left: left - 4,
|
||||
top: top - 4,
|
||||
minWidth: boxWidth + 8,
|
||||
left: left - 2,
|
||||
top: top - 2,
|
||||
minWidth: (boxWidth + 4) * 10,
|
||||
transform: 'scale(0.1)',
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
@@ -146,12 +155,11 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
|
||||
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||||
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
|
||||
}}
|
||||
className="w-full bg-transparent outline-none m-0 p-0"
|
||||
className="w-full bg-transparent outline-none m-0 px-[10px]"
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: `${fontSizeScreen}px`,
|
||||
lineHeight: `${boxHeight}px`,
|
||||
minWidth: '100px',
|
||||
fontFamily,
|
||||
fontSize: `${fontSizeScreen * 10}px`,
|
||||
lineHeight: `${boxHeight * 10}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user