done Image XObject handler (Do operator)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
Reference in New Issue
Block a user