content completed

This commit is contained in:
saqib mir
2026-06-17 11:03:47 +05:30
parent 20ee7fbe20
commit a39c15d0be
4 changed files with 76 additions and 42 deletions
+1
View File
@@ -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)
+40 -15
View File
@@ -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);
+1 -1
View File
@@ -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.
+34 -26
View File
@@ -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>