diff --git a/CMakeLists.txt b/CMakeLists.txt index abff300..9d483f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/bindings/python/pdfengine_py.cpp b/bindings/python/pdfengine_py.cpp index 9af0551..747047d 100644 --- a/bindings/python/pdfengine_py.cpp +++ b/bindings/python/pdfengine_py.cpp @@ -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(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::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); diff --git a/frontend/src/lib/gatewayService.ts b/frontend/src/lib/gatewayService.ts index 2e0875a..ecb2326 100644 --- a/frontend/src/lib/gatewayService.ts +++ b/frontend/src/lib/gatewayService.ts @@ -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. diff --git a/frontend/src/viewer/StreamEditLayer.tsx b/frontend/src/viewer/StreamEditLayer.tsx index d87c264..b8f059f 100644 --- a/frontend/src/viewer/StreamEditLayer.tsx +++ b/frontend/src/viewer/StreamEditLayer.tsx @@ -62,22 +62,18 @@ export const StreamEditLayer: React.FC = ({ 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 = ({ setEditingIndex(null); }; + const uniqueFonts = Array.from(new Set(objects.map(o => o.fontName).filter(Boolean))); + if (loading) { return null; } return (
+ {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 = ({ 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 = ({ 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 (
@@ -124,16 +132,17 @@ export const StreamEditLayer: React.FC = ({ }} title={`Font: ${obj.fontName}, Size: ${obj.fontSize}`} > - {/* Visual debug text, mostly transparent to see what's underneath but know where box is */} {obj.text}
) : (
= ({ 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`, }} />