done Image XObject handler (Do operator)

This commit is contained in:
azeeee05
2026-06-11 11:24:35 +05:30
parent 98e3a2f89c
commit addc888545
11 changed files with 410 additions and 2 deletions
+1
View File
@@ -16,6 +16,7 @@ add_library(pdfengine STATIC
src/core/skia_renderer.cpp
src/parser/pdfium_loader.cpp
src/parser/pdfium_document.cpp
src/parser/content_stream_parser.cpp
src/fonts/face/font_face.cpp
src/fonts/face/free_type_manager.cpp
src/fonts/loader/font_resolver.cpp
+28 -1
View File
@@ -4,7 +4,8 @@
#include <memory>
#include <string>
#include <pdfengine/graphics_state.hpp>
#include <pdfengine/path.hpp>
#include <pdfengine/image.hpp>
namespace pdfengine {
class CommandVisitor;
@@ -45,6 +46,26 @@ struct DrawTextCommand : public Command {
void accept(CommandVisitor& visitor) const override;
};
struct FillPathCommand : public Command {
Path path;
explicit FillPathCommand(Path path) : path(std::move(path)) {}
void accept(CommandVisitor& visitor) const override;
};
struct StrokePathCommand : public Command {
Path path;
explicit StrokePathCommand(Path path) : path(std::move(path)) {}
void accept(CommandVisitor& visitor) const override;
};
struct DrawImageCommand : public Command {
ImageInfo image;
float x, y, width, height;
DrawImageCommand(ImageInfo img, float x, float y, float w, float h)
: image(std::move(img)), x(x), y(y), width(w), height(h) {}
void accept(CommandVisitor& visitor) const override;
};
// --- Visitor Interface ---
// The visitor interface that the renderer (or replay engine) implements
@@ -56,6 +77,9 @@ public:
virtual void visit(const SetTransformCommand& cmd) = 0;
virtual void visit(const FillRectCommand& cmd) = 0;
virtual void visit(const DrawTextCommand& cmd) = 0;
virtual void visit(const FillPathCommand& cmd) = 0;
virtual void visit(const StrokePathCommand& cmd) = 0;
virtual void visit(const DrawImageCommand& cmd) = 0;
};
// --- Display List Container ---
@@ -77,6 +101,9 @@ public:
void setTransform(const Matrix& m);
void fillRect(float x, float y, float w, float h);
void drawText(const std::string& text, float x, float y);
void fillPath(const Path& path);
void strokePath(const Path& path);
void drawImage(const ImageInfo& image, float x, float y, float w, float h);
[[nodiscard]] size_t size() const noexcept { return m_commands.size(); }
void clear() { m_commands.clear(); }
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <vector>
#include <cstdint>
namespace pdfengine {
// Basic color spaces we might encounter
enum class ColorSpace {
DeviceGray,
DeviceRGB,
DeviceCMYK,
Indexed
};
// Holds decoded image data ready for the display list (typically RGBA)
struct ImageInfo {
int width = 0;
int height = 0;
int channels = 4; // 4 = RGBA
std::vector<uint8_t> pixelData; // Decoded raw pixels
};
} // namespace pdfengine
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <vector>
namespace pdfengine {
// Basic point structure
struct Point {
float x = 0.0f;
float y = 0.0f;
};
// Represents a 2D vector path constructed from basic drawing commands.
class Path {
public:
enum class Verb {
MoveTo,
LineTo,
CubicBezierTo,
Close
};
struct Segment {
Verb verb;
Point points[3]; // Up to 3 points depending on verb (e.g., Cubic bezier)
};
Path() = default;
void moveTo(float x, float y) {
m_segments.push_back({Verb::MoveTo, {{x, y}, {}, {}}});
}
void lineTo(float x, float y) {
m_segments.push_back({Verb::LineTo, {{x, y}, {}, {}}});
}
void cubicTo(float cp1x, float cp1y, float cp2x, float cp2y, float x, float y) {
m_segments.push_back({Verb::CubicBezierTo, {{cp1x, cp1y}, {cp2x, cp2y}, {x, y}}});
}
void close() {
m_segments.push_back({Verb::Close, {{}, {}, {}}});
}
// Helper for 're' (rectangle) operator
void addRect(float x, float y, float w, float h) {
moveTo(x, y);
lineTo(x + w, y);
lineTo(x + w, y + h);
lineTo(x, y + h);
close();
}
void clear() {
m_segments.clear();
}
[[nodiscard]] const std::vector<Segment>& segments() const { return m_segments; }
[[nodiscard]] bool empty() const { return m_segments.empty(); }
private:
std::vector<Segment> m_segments;
};
} // namespace pdfengine
+3 -1
View File
@@ -19,7 +19,9 @@ public:
void visit(const SetTransformCommand& cmd) override;
void visit(const FillRectCommand& cmd) override;
void visit(const DrawTextCommand& cmd) override;
void visit(const FillPathCommand& cmd) override;
void visit(const StrokePathCommand& cmd) override;
void visit(const DrawImageCommand& cmd) override;
// Renders the entire display list to the canvas
void render(const DisplayList& displayList);
+15
View File
@@ -7,6 +7,9 @@ void RestoreStateCommand::accept(CommandVisitor& visitor) const { visitor.visit(
void SetTransformCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void FillRectCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void DrawTextCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void FillPathCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void StrokePathCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void DrawImageCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); }
void DisplayList::addCommand(std::unique_ptr<Command> cmd) {
if (cmd) {
@@ -40,4 +43,16 @@ void DisplayList::drawText(const std::string& text, float x, float y) {
addCommand(std::make_unique<DrawTextCommand>(text, x, y));
}
void DisplayList::fillPath(const Path& path) {
addCommand(std::make_unique<FillPathCommand>(path));
}
void DisplayList::strokePath(const Path& path) {
addCommand(std::make_unique<StrokePathCommand>(path));
}
void DisplayList::drawImage(const ImageInfo& image, float x, float y, float w, float h) {
addCommand(std::make_unique<DrawImageCommand>(image, x, y, w, h));
}
} // namespace pdfengine
+120
View File
@@ -6,6 +6,10 @@
#include <include/core/SkMatrix.h>
#include <include/core/SkFont.h>
#include <include/core/SkTypeface.h>
#include <include/core/SkPath.h>
#include <include/core/SkImage.h>
#include <include/core/SkData.h>
#include <include/core/SkImageInfo.h>
#endif
namespace pdfengine {
@@ -93,4 +97,120 @@ void SkiaRenderer::visit(const DrawTextCommand& cmd) {
#endif
}
void SkiaRenderer::visit(const FillPathCommand& cmd) {
#ifdef PDFENGINE_WITH_SKIA
if (!m_canvas || cmd.path.empty()) return;
SkPath skPath;
for (const auto& segment : cmd.path.segments()) {
switch (segment.verb) {
case Path::Verb::MoveTo:
skPath.moveTo(segment.points[0].x, segment.points[0].y);
break;
case Path::Verb::LineTo:
skPath.lineTo(segment.points[0].x, segment.points[0].y);
break;
case Path::Verb::CubicBezierTo:
skPath.cubicTo(
segment.points[0].x, segment.points[0].y,
segment.points[1].x, segment.points[1].y,
segment.points[2].x, segment.points[2].y
);
break;
case Path::Verb::Close:
skPath.close();
break;
}
}
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kFill_Style);
const auto& color = m_stateStack.current().fillColor;
paint.setColor(SkColorSetARGB(255,
static_cast<uint8_t>(color.r * 255),
static_cast<uint8_t>(color.g * 255),
static_cast<uint8_t>(color.b * 255)));
m_canvas->drawPath(skPath, paint);
#endif
}
void SkiaRenderer::visit(const StrokePathCommand& cmd) {
#ifdef PDFENGINE_WITH_SKIA
if (!m_canvas || cmd.path.empty()) return;
SkPath skPath;
for (const auto& segment : cmd.path.segments()) {
switch (segment.verb) {
case Path::Verb::MoveTo:
skPath.moveTo(segment.points[0].x, segment.points[0].y);
break;
case Path::Verb::LineTo:
skPath.lineTo(segment.points[0].x, segment.points[0].y);
break;
case Path::Verb::CubicBezierTo:
skPath.cubicTo(
segment.points[0].x, segment.points[0].y,
segment.points[1].x, segment.points[1].y,
segment.points[2].x, segment.points[2].y
);
break;
case Path::Verb::Close:
skPath.close();
break;
}
}
SkPaint paint;
paint.setAntiAlias(true);
paint.setStyle(SkPaint::kStroke_Style);
// In a real implementation we would use strokeColor and strokeWidth from graphics state
const auto& color = m_stateStack.current().fillColor;
paint.setColor(SkColorSetARGB(255,
static_cast<uint8_t>(color.r * 255),
static_cast<uint8_t>(color.g * 255),
static_cast<uint8_t>(color.b * 255)));
paint.setStrokeWidth(1.0f); // Default for now
m_canvas->drawPath(skPath, paint);
#endif
}
void SkiaRenderer::visit(const DrawImageCommand& cmd) {
#ifdef PDFENGINE_WITH_SKIA
if (!m_canvas || cmd.image.pixelData.empty()) return;
// Create an SkImage from the raw RGBA pixels
SkImageInfo info = SkImageInfo::Make(
cmd.image.width,
cmd.image.height,
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType
);
sk_sp<SkData> data = SkData::MakeWithCopy(
cmd.image.pixelData.data(),
cmd.image.pixelData.size()
);
sk_sp<SkImage> skImage = SkImages::RasterFromData(
info,
std::move(data),
cmd.image.width * 4 // rowBytes
);
if (skImage) {
SkRect destRect = SkRect::MakeXYWH(cmd.x, cmd.y, cmd.width, cmd.height);
m_canvas->drawImageRect(
skImage.get(),
destRect,
SkSamplingOptions(SkFilterMode::kLinear)
);
}
#endif
}
} // namespace pdfengine
+122
View File
@@ -0,0 +1,122 @@
#include "content_stream_parser.hpp"
#include <sstream>
#include <iostream>
#include <cctype>
namespace pdfengine {
std::vector<std::string> ContentStreamParser::tokenize(const std::string& stream) {
std::vector<std::string> tokens;
std::string current_token;
for (size_t i = 0; i < stream.size(); ++i) {
char c = stream[i];
// Simplified tokenization: split by whitespace
// In a real PDF parser, we must handle arrays [], dicts <<>>, strings (), etc.
if (std::isspace(c)) {
if (!current_token.empty()) {
tokens.push_back(current_token);
current_token.clear();
}
} else if (c == '[' || c == ']' || c == '<' || c == '>') {
if (!current_token.empty()) {
tokens.push_back(current_token);
current_token.clear();
}
tokens.push_back(std::string(1, c));
if (c == '<' && i + 1 < stream.size() && stream[i+1] == '<') {
tokens.back() = "<<";
i++;
} else if (c == '>' && i + 1 < stream.size() && stream[i+1] == '>') {
tokens.back() = ">>";
i++;
}
} else {
current_token += c;
}
}
if (!current_token.empty()) {
tokens.push_back(current_token);
}
return tokens;
}
void ContentStreamParser::parse(const std::string& contentStream, DisplayList& displayList) {
std::vector<std::string> tokens = tokenize(contentStream);
std::vector<std::string> operands;
for (const auto& token : tokens) {
// Simple heuristic: if it starts with a letter (and isn't a PDF name starting with /)
// or is a known operator, treat as operator. Otherwise operand.
if (!token.empty() && std::isalpha(token[0]) && token[0] != '/') {
// It's an operator
if (token == "m") {
if (operands.size() >= 2) {
float y = std::stof(operands.back()); operands.pop_back();
float x = std::stof(operands.back()); operands.pop_back();
m_currentPath.moveTo(x, y);
}
} else if (token == "l") {
if (operands.size() >= 2) {
float y = std::stof(operands.back()); operands.pop_back();
float x = std::stof(operands.back()); operands.pop_back();
m_currentPath.lineTo(x, y);
}
} else if (token == "c") {
if (operands.size() >= 6) {
float y3 = std::stof(operands.back()); operands.pop_back();
float x3 = std::stof(operands.back()); operands.pop_back();
float y2 = std::stof(operands.back()); operands.pop_back();
float x2 = std::stof(operands.back()); operands.pop_back();
float y1 = std::stof(operands.back()); operands.pop_back();
float x1 = std::stof(operands.back()); operands.pop_back();
m_currentPath.cubicTo(x1, y1, x2, y2, x3, y3);
}
} else if (token == "re") {
if (operands.size() >= 4) {
float h = std::stof(operands.back()); operands.pop_back();
float w = std::stof(operands.back()); operands.pop_back();
float y = std::stof(operands.back()); operands.pop_back();
float x = std::stof(operands.back()); operands.pop_back();
m_currentPath.addRect(x, y, w, h);
}
} else if (token == "S" || token == "s") {
if (token == "s") m_currentPath.close();
displayList.strokePath(m_currentPath);
m_currentPath.clear();
} else if (token == "f" || token == "F") {
displayList.fillPath(m_currentPath);
m_currentPath.clear();
} else if (token == "B" || token == "b") {
if (token == "b") m_currentPath.close();
displayList.fillPath(m_currentPath);
displayList.strokePath(m_currentPath);
m_currentPath.clear();
} else if (token == "Do") {
// Draw Image XObject
if (!operands.empty()) {
std::string imageName = operands.back(); operands.pop_back();
// In a full implementation, we would look up 'imageName' in the
// page's /Resources /XObject dictionary, check if /Subtype is /Image,
// apply /Filter /DCTDecode (JPEG decompression), and extract raw pixels.
// For now, we emit a placeholder command if the name is found.
// Dummy ImageInfo for stub
ImageInfo img;
img.width = 100;
img.height = 100;
// Usually the CTM (Current Transformation Matrix) defines the image bounds.
// We just emit a 1x1 image at origin, assuming SetTransformCommand handled bounds.
displayList.drawImage(img, 0.0f, 0.0f, 1.0f, 1.0f);
}
}
// Clear operands for next operator
operands.clear();
} else {
operands.push_back(token);
}
}
}
} // namespace pdfengine
@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <vector>
#include <pdfengine/display_list.hpp>
#include <pdfengine/path.hpp>
namespace pdfengine {
// A lightweight parser for PDF content streams.
// In Phase 3, this interprets operators and builds the DisplayList.
class ContentStreamParser {
public:
ContentStreamParser() = default;
// Parses the given content stream and appends commands to the display list.
// 'resources' could later be a map of names to images/fonts.
void parse(const std::string& contentStream, DisplayList& displayList);
private:
// Helper to tokenize the content stream
std::vector<std::string> tokenize(const std::string& stream);
// Current path state
Path m_currentPath;
};
} // namespace pdfengine
+3
View File
@@ -15,6 +15,9 @@ public:
void visit(const SetTransformCommand& cmd) override { calls.push_back("SetTransform(" + std::to_string(cmd.matrix.a) + ")"); }
void visit(const FillRectCommand& cmd) override { calls.push_back("FillRect(" + std::to_string(cmd.width) + ")"); }
void visit(const DrawTextCommand& cmd) override { calls.push_back("DrawText(" + cmd.text + ")"); }
void visit(const FillPathCommand&) override { calls.push_back("FillPath"); }
void visit(const StrokePathCommand&) override { calls.push_back("StrokePath"); }
void visit(const DrawImageCommand&) override { calls.push_back("DrawImage"); }
};
TEST(DisplayListTest, RecordAndReplay) {
Binary file not shown.