fix the issue

This commit is contained in:
saqib mir
2026-06-22 10:17:51 +05:30
parent 8e9b0c8dfb
commit e1e1c91736
8 changed files with 321 additions and 35 deletions
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <memory>
#include <pdfengine/graphics_state.hpp>
#include <pdfengine/path.hpp>
namespace pdfengine {
@@ -59,4 +60,19 @@ public:
Matrix transform;
};
enum class PathPaintOp {
Stroke,
Fill,
FillStroke
};
class PathObject : public ContentObject {
public:
ContentObjectType getType() const override { return ContentObjectType::Path; }
Path path;
PathPaintOp paintOp = PathPaintOp::Stroke;
Matrix transform;
};
} // namespace pdfengine
+99
View File
@@ -39,6 +39,20 @@ void ContentBuilder::processOperation(const Operation& op, std::vector<std::uniq
handleTJ_Array(op, outObjects);
} else if (op.op == "Do") {
handleDo(op, outObjects);
} else if (op.op == "m" || op.op == "l" || op.op == "c" || op.op == "re" || op.op == "h") {
handlePathConstruction(op);
} else if (op.op == "S") {
handlePathPaint(PathPaintOp::Stroke, outObjects);
} else if (op.op == "s") {
handlePathPaint(PathPaintOp::Stroke, outObjects, true);
} else if (op.op == "f" || op.op == "F" || op.op == "f*") {
handlePathPaint(PathPaintOp::Fill, outObjects);
} else if (op.op == "B" || op.op == "B*") {
handlePathPaint(PathPaintOp::FillStroke, outObjects);
} else if (op.op == "b" || op.op == "b*") {
handlePathPaint(PathPaintOp::FillStroke, outObjects, true);
} else if (op.op == "n") {
currentPath_.clear();
}
}
@@ -207,4 +221,89 @@ void ContentBuilder::handleDo(const Operation& op, std::vector<std::unique_ptr<C
}
}
void ContentBuilder::handlePathConstruction(const Operation& op) {
auto numberOperand = [&op](size_t index, float& value) {
if (index >= op.operands.size() || op.operands[index]->type != AstNodeType::Number) {
return false;
}
value = static_cast<float>(op.operands[index]->numberValue);
return true;
};
if (op.op == "h") {
if (!currentPath_.empty()) {
currentPath_.close();
}
return;
}
if (op.op == "m" || op.op == "l") {
if (op.operands.size() < 2) return;
float x = 0.0f;
float y = 0.0f;
if (!numberOperand(op.operands.size() - 2, x) || !numberOperand(op.operands.size() - 1, y)) {
return;
}
if (op.op == "m") {
currentPath_.moveTo(x, y);
} else {
currentPath_.lineTo(x, y);
}
return;
}
if (op.op == "c") {
if (op.operands.size() < 6) return;
float x1 = 0.0f;
float y1 = 0.0f;
float x2 = 0.0f;
float y2 = 0.0f;
float x3 = 0.0f;
float y3 = 0.0f;
const size_t start = op.operands.size() - 6;
if (!numberOperand(start + 0, x1) || !numberOperand(start + 1, y1) ||
!numberOperand(start + 2, x2) || !numberOperand(start + 3, y2) ||
!numberOperand(start + 4, x3) || !numberOperand(start + 5, y3)) {
return;
}
currentPath_.cubicTo(x1, y1, x2, y2, x3, y3);
return;
}
if (op.op == "re") {
if (op.operands.size() < 4) return;
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
const size_t start = op.operands.size() - 4;
if (!numberOperand(start + 0, x) || !numberOperand(start + 1, y) ||
!numberOperand(start + 2, width) || !numberOperand(start + 3, height)) {
return;
}
currentPath_.addRect(x, y, width, height);
}
}
void ContentBuilder::handlePathPaint(PathPaintOp paintOp,
std::vector<std::unique_ptr<ContentObject>>& outObjects,
bool closePath) {
if (closePath) {
if (!currentPath_.empty()) {
currentPath_.close();
}
}
if (currentPath_.empty()) {
return;
}
auto pathObj = std::make_unique<PathObject>();
pathObj->path = currentPath_;
pathObj->paintOp = paintOp;
pathObj->transform = state_.ctm;
outObjects.push_back(std::move(pathObj));
currentPath_.clear();
}
} // namespace pdfengine
+3
View File
@@ -27,6 +27,7 @@ private:
GraphicsState state_;
std::vector<GraphicsState> stateStack_;
ResourceResolver* resolver_;
Path currentPath_;
void processOperation(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
@@ -37,6 +38,8 @@ private:
void handleTJ_Array(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
void handleCm(const Operation& op);
void handleDo(const Operation& op, std::vector<std::unique_ptr<ContentObject>>& outObjects);
void handlePathConstruction(const Operation& op);
void handlePathPaint(PathPaintOp paintOp, std::vector<std::unique_ptr<ContentObject>>& outObjects, bool closePath = false);
};
} // namespace pdfengine
+114
View File
@@ -14,6 +14,26 @@
using namespace pdfengine;
namespace {
std::vector<std::unique_ptr<ContentObject>> buildObjects(const std::string& content) {
Lexer lexer(content);
auto tokens = lexer.tokenize();
ContentParser parser(tokens);
ContentBuilder builder;
return builder.build(parser.parse());
}
const PathObject* requirePathObject(const std::unique_ptr<ContentObject>& object) {
EXPECT_EQ(object->getType(), ContentObjectType::Path);
if (object->getType() != ContentObjectType::Path) {
return nullptr;
}
return static_cast<const PathObject*>(object.get());
}
} // namespace
TEST(ContentBuilderTest, SimpleTextState) {
Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj");
auto tokens = lexer.tokenize();
@@ -73,6 +93,100 @@ TEST(ContentBuilderTest, RotatedTextMatrix) {
EXPECT_DOUBLE_EQ(textObj->tm[5], 200.0);
}
TEST(ContentBuilderTest, StrokePathFromMoveAndLine) {
auto objects = buildObjects("10 20 m 30 40 l S");
ASSERT_EQ(objects.size(), 1);
const auto* pathObj = requirePathObject(objects[0]);
ASSERT_NE(pathObj, nullptr);
EXPECT_EQ(pathObj->paintOp, PathPaintOp::Stroke);
const auto& segments = pathObj->path.segments();
ASSERT_EQ(segments.size(), 2);
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
EXPECT_FLOAT_EQ(segments[0].points[0].x, 10.0f);
EXPECT_FLOAT_EQ(segments[0].points[0].y, 20.0f);
EXPECT_EQ(segments[1].verb, Path::Verb::LineTo);
EXPECT_FLOAT_EQ(segments[1].points[0].x, 30.0f);
EXPECT_FLOAT_EQ(segments[1].points[0].y, 40.0f);
}
TEST(ContentBuilderTest, FillRectanglePath) {
auto objects = buildObjects("5 6 7 8 re f");
ASSERT_EQ(objects.size(), 1);
const auto* pathObj = requirePathObject(objects[0]);
ASSERT_NE(pathObj, nullptr);
EXPECT_EQ(pathObj->paintOp, PathPaintOp::Fill);
const auto& segments = pathObj->path.segments();
ASSERT_EQ(segments.size(), 5);
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
EXPECT_FLOAT_EQ(segments[0].points[0].x, 5.0f);
EXPECT_FLOAT_EQ(segments[0].points[0].y, 6.0f);
EXPECT_EQ(segments[1].verb, Path::Verb::LineTo);
EXPECT_FLOAT_EQ(segments[1].points[0].x, 12.0f);
EXPECT_FLOAT_EQ(segments[1].points[0].y, 6.0f);
EXPECT_EQ(segments[2].verb, Path::Verb::LineTo);
EXPECT_FLOAT_EQ(segments[2].points[0].x, 12.0f);
EXPECT_FLOAT_EQ(segments[2].points[0].y, 14.0f);
EXPECT_EQ(segments[3].verb, Path::Verb::LineTo);
EXPECT_FLOAT_EQ(segments[3].points[0].x, 5.0f);
EXPECT_FLOAT_EQ(segments[3].points[0].y, 14.0f);
EXPECT_EQ(segments[4].verb, Path::Verb::Close);
}
TEST(ContentBuilderTest, FillStrokeCubicPath) {
auto objects = buildObjects("1 2 m 3 4 5 6 7 8 c B");
ASSERT_EQ(objects.size(), 1);
const auto* pathObj = requirePathObject(objects[0]);
ASSERT_NE(pathObj, nullptr);
EXPECT_EQ(pathObj->paintOp, PathPaintOp::FillStroke);
const auto& segments = pathObj->path.segments();
ASSERT_EQ(segments.size(), 2);
EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo);
EXPECT_EQ(segments[1].verb, Path::Verb::CubicBezierTo);
EXPECT_FLOAT_EQ(segments[1].points[0].x, 3.0f);
EXPECT_FLOAT_EQ(segments[1].points[0].y, 4.0f);
EXPECT_FLOAT_EQ(segments[1].points[1].x, 5.0f);
EXPECT_FLOAT_EQ(segments[1].points[1].y, 6.0f);
EXPECT_FLOAT_EQ(segments[1].points[2].x, 7.0f);
EXPECT_FLOAT_EQ(segments[1].points[2].y, 8.0f);
}
TEST(ContentBuilderTest, PathPaintClearsCurrentPath) {
auto objects = buildObjects("0 0 m 10 10 l S 20 20 m 30 30 l f");
ASSERT_EQ(objects.size(), 2);
const auto* stroke = requirePathObject(objects[0]);
const auto* fill = requirePathObject(objects[1]);
ASSERT_NE(stroke, nullptr);
ASSERT_NE(fill, nullptr);
EXPECT_EQ(stroke->paintOp, PathPaintOp::Stroke);
EXPECT_EQ(fill->paintOp, PathPaintOp::Fill);
ASSERT_EQ(stroke->path.segments().size(), 2);
ASSERT_EQ(fill->path.segments().size(), 2);
EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].x, 20.0f);
EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].y, 20.0f);
}
TEST(ContentBuilderTest, PathCapturesCurrentTransform) {
auto objects = buildObjects("q 2 0 0 3 10 20 cm 1 2 3 4 re f Q");
ASSERT_EQ(objects.size(), 1);
const auto* pathObj = requirePathObject(objects[0]);
ASSERT_NE(pathObj, nullptr);
EXPECT_FLOAT_EQ(pathObj->transform.a, 2.0f);
EXPECT_FLOAT_EQ(pathObj->transform.b, 0.0f);
EXPECT_FLOAT_EQ(pathObj->transform.c, 0.0f);
EXPECT_FLOAT_EQ(pathObj->transform.d, 3.0f);
EXPECT_FLOAT_EQ(pathObj->transform.e, 10.0f);
EXPECT_FLOAT_EQ(pathObj->transform.f, 20.0f);
}
TEST(ContentBuilderTest, IntegrationHelloWorld) {
pdfengine::qpdf_layer::QpdfExtractor extractor;
std::filesystem::path path = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf";
+57 -16
View File
@@ -721,6 +721,62 @@ TEST(ImageXObjectVerification, LoadRenderCloseStress100Pages1000ImageDraws) {
const auto pdf = makePdf(std::move(objects));
auto verifyCustomDecode = [&](std::string_view phase) {
QPDF qpdf;
qpdf.processMemoryFile("image-xobject-stress", reinterpret_cast<const char*>(pdf.data()), pdf.size());
auto pages = qpdf.getAllPages();
ASSERT_EQ(pages.size(), static_cast<size_t>(kPages)) << phase;
size_t decodedImages = 0;
for (size_t pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
const auto& page = pages[pageIndex];
const std::string content = decodedStream(page.getKey("/Contents"));
Lexer lexer(content);
auto tokens = lexer.tokenize();
ContentParser parser(tokens);
auto operations = parser.parse();
size_t doOps = 0;
std::vector<std::string> doNames;
for (const auto& operation : operations) {
if (operation.op == "Do") {
++doOps;
if (!operation.operands.empty() && operation.operands.back()->type == AstNodeType::Name) {
doNames.push_back(operation.operands.back()->stringValue);
}
}
}
QpdfResourceResolver resolver(page.getKey("/Resources"));
std::vector<std::string> unresolved;
for (int imageIndex = 0; imageIndex < kImagesPerPage; ++imageIndex) {
const std::string name = "Im" + std::to_string(imageIndex);
if (resolver.resolveXObject(name).type != XObjectType::Image) {
unresolved.push_back(name);
}
}
auto pageObjects = buildPageObjects(page);
auto parsedImages = imageObjects(pageObjects);
std::vector<std::string> emittedNames;
for (const auto* image : parsedImages) {
emittedNames.push_back(image->name);
}
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage))
<< phase << " page=" << pageIndex
<< " doOps=" << doOps
<< " doNames=" << ::testing::PrintToString(doNames)
<< " unresolved=" << ::testing::PrintToString(unresolved)
<< " emitted=" << ::testing::PrintToString(emittedNames)
<< " decoded=[" << content << "]"
<< " resources=[" << page.getKey("/Resources").unparse() << "]";
for (const auto* image : parsedImages) {
EXPECT_EQ(image->pixelData.size(), 4u) << phase << " page=" << pageIndex;
}
decodedImages += parsedImages.size();
}
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage)) << phase;
};
verifyCustomDecode("before-pdfium-render");
auto document = PdfDocument::loadFromMemory(pdf);
if (!document) {
GTEST_SKIP() << "PDFium load unavailable in this build";
@@ -734,20 +790,5 @@ TEST(ImageXObjectVerification, LoadRenderCloseStress100Pages1000ImageDraws) {
}
document = {};
QPDF qpdf;
qpdf.processMemoryFile("image-xobject-stress", reinterpret_cast<const char*>(pdf.data()), pdf.size());
auto pages = qpdf.getAllPages();
ASSERT_EQ(pages.size(), static_cast<size_t>(kPages));
size_t decodedImages = 0;
for (const auto& page : pages) {
auto pageObjects = buildPageObjects(page);
auto parsedImages = imageObjects(pageObjects);
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage));
for (const auto* image : parsedImages) {
EXPECT_EQ(image->pixelData.size(), 4u);
}
decodedImages += parsedImages.size();
}
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage));
verifyCustomDecode("after-pdfium-render");
}
+27 -14
View File
@@ -3,6 +3,8 @@ import { gatewayService } from '../lib/gatewayService';
import type { TextObjectResponse } from '../lib/gatewayService';
import { toast } from '../lib/toast';
import { loadPdfFont } from '../lib/fontFaceLoader';
// Measure a run's width + ascent/descent in its actual font (memoized). Sizes are returned in the
// same units as `sizePx`, so callers scale by the text matrix + zoom. Before the embedded @font-face
// loads this measures the fallback chain (still far better than a char-count guess); clearMeasureCache
@@ -53,6 +55,7 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [value, setValue] = useState('');
const [fontsReady, setFontsReady] = useState(false);
const [fontFamilyMap, setFontFamilyMap] = useState<Record<string, string>>({});
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -124,13 +127,30 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
useEffect(() => {
if (!uniqueFonts.length) return;
let active = true;
const fonts = (document as unknown as { fonts?: { load: (f: string) => Promise<unknown> } }).fonts;
if (!fonts) return;
Promise.all(uniqueFonts.map(fn => fonts.load(`16px 'PDF_${fn}'`).catch(() => {})))
.then(() => { if (active) { clearMeasureCache(); setFontsReady(v => !v); } });
Promise.all(uniqueFonts.map(async (fn) => {
const cssFamily = await loadPdfFont(documentId, fn);
return { fn, cssFamily };
})).then((results) => {
if (!active) return;
let changed = false;
const newMap: Record<string, string> = { ...fontFamilyMap };
for (const res of results) {
if (res.cssFamily && newMap[res.fn] !== res.cssFamily) {
newMap[res.fn] = res.cssFamily;
changed = true;
}
}
if (changed) {
setFontFamilyMap(newMap);
clearMeasureCache();
setFontsReady(v => !v);
}
});
return () => { active = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uniqueFontsKey]);
}, [uniqueFontsKey, documentId]);
if (loading) {
return null;
@@ -138,14 +158,6 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
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];
@@ -160,7 +172,8 @@ export const StreamEditLayer: React.FC<StreamEditLayerProps> = ({
const fontSizeScreen = obj.fontSize * scaleY * zoom;
const isEditing = editingIndex === i;
const fontFamily = obj.fontName ? `'PDF_${obj.fontName}', sans-serif` : 'sans-serif';
const cssFam = obj.fontName ? fontFamilyMap[obj.fontName] : null;
const fontFamily = cssFam ? `'${cssFam}', sans-serif` : 'sans-serif';
// P2b — EXACT hit-box from real font metrics (measured in the actual embedded font once
// it loads; falls back to the sans-serif chain before then) instead of the old
+4 -4
View File
@@ -268,18 +268,18 @@ def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
# Cap the id length; never echo it back into error bodies.
if not internal_font_id or len(internal_font_id) > 256:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
return Response(status_code=status.HTTP_204_NO_CONTENT)
doc_info = document_store.get_document(document_id)
if not doc_info:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document not found")
return Response(status_code=status.HTTP_204_NO_CONTENT)
try:
data = bytes(doc_info["doc_instance"].get_font_data(internal_font_id))
except Exception:
data = b""
if not data:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not found")
return Response(status_code=status.HTTP_204_NO_CONTENT)
magic = data[:4]
if magic in _SFNT_TTF_MAGIC:
@@ -288,7 +288,7 @@ def get_font_bytes(document_id: str, internal_font_id: str) -> Response:
media_type = "font/otf"
else:
# Type1 (\x80\x01 / "%!") or anything not sfnt-wrapped — not browser-loadable.
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Font not loadable")
return Response(status_code=status.HTTP_204_NO_CONTENT)
etag = '"' + hashlib.sha256(data).hexdigest()[:32] + '"'
return Response(