This commit is contained in:
Furqan-14
2026-06-12 11:31:40 +05:30
13 changed files with 296 additions and 14 deletions
+1
View File
@@ -17,6 +17,7 @@ add_library(pdfengine STATIC
src/parser/pdfium_loader.cpp
src/parser/pdfium_document.cpp
src/parser/content_stream_parser.cpp
src/parser/decoration_builder.cpp
src/text/selection.cpp
src/fonts/face/font_face.cpp
src/fonts/face/free_type_manager.cpp
+55
View File
@@ -0,0 +1,55 @@
#include "decoration_builder.hpp"
namespace pdfengine {
Path DecorationBuilder::buildUnderline(float x, float y, float width, float thickness, float offset) {
Path p;
// An underline is essentially a thin filled rectangle at (y + offset).
// The caller is responsible for supplying the correctly signed offset.
p.addRect(x, y + offset, width, thickness);
return p;
}
Path DecorationBuilder::buildStrikeout(float x, float y, float width, float thickness, float offset) {
Path p;
// A strikeout is similarly a thin filled rectangle, positioned higher up.
p.addRect(x, y + offset, width, thickness);
return p;
}
Path DecorationBuilder::buildSquiggly(float x, float y, float width, float amplitude, float frequency) {
Path p;
if (width <= 0.0f) {
return p;
}
p.moveTo(x, y);
float currentX = x;
float endX = x + width;
// Create a jagged squiggly line using line segments.
// This is drawn as a stroked path rather than a filled rect.
bool up = true;
while (currentX < endX) {
float nextX = currentX + (frequency / 2.0f);
if (nextX > endX) {
nextX = endX;
// Adjust the final Y to keep the slope somewhat consistent if chopped early
float ratio = (nextX - currentX) / (frequency / 2.0f);
float nextY = y + (up ? amplitude : -amplitude) * ratio;
p.lineTo(nextX, nextY);
break;
}
float nextY = y + (up ? amplitude : -amplitude);
p.lineTo(nextX, nextY);
currentX = nextX;
up = !up;
}
return p;
}
} // namespace pdfengine
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <pdfengine/path.hpp>
namespace pdfengine {
// A utility class to generate vector paths for text markup and decorations
class DecorationBuilder {
public:
// Builds a path representing a straight underline.
// x, y: starting coordinates (usually the baseline origin)
// width: length of the underline
// thickness: thickness of the line (used to build a thin rectangle)
// offset: vertical offset from y
static Path buildUnderline(float x, float y, float width, float thickness = 1.0f, float offset = -2.0f);
// Builds a path representing a strikeout line.
// x, y: starting coordinates (baseline)
// width: length of the strikeout
// thickness: thickness of the line
// offset: vertical offset from y (typically goes up through the text)
static Path buildStrikeout(float x, float y, float width, float thickness = 1.0f, float offset = 4.0f);
// Builds a path representing a squiggly underline (often used for spelling or grammar highlights).
// x, y: starting coordinates
// width: length of the squiggly
// amplitude: height of the squiggly waves
// frequency: horizontal width of a single wave cycle
static Path buildSquiggly(float x, float y, float width, float amplitude = 2.0f, float frequency = 4.0f);
};
} // namespace pdfengine
+75
View File
@@ -17,6 +17,7 @@
#include "fonts/pdf_fonts/font_fallback.hpp"
#include "fonts/pdf_fonts/font_subset.hpp"
#include "fonts/shaping/hb_shaper.hpp"
#include "decoration_builder.hpp"
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
@@ -2261,6 +2262,80 @@ std::expected<void, EngineError> PdfiumDocument::applyEdits(const std::string& e
return std::unexpected(EngineError::Unknown);
}
FPDF_ClosePage(page);
} else if (type == "underline" || type == "strikeout" || type == "squiggly") {
if (!op.contains("data") || !op["data"].is_object()) {
spdlog::error("{} operation missing 'data' object", type);
return std::unexpected(EngineError::InvalidFormat);
}
auto data = op["data"];
double x = data.value("x", 0.0);
double y = data.value("y", 0.0);
double width = data.value("width", 0.0);
double thickness = data.value("thickness", 1.0);
std::string color = data.value("color", "#000000");
spdlog::info("Parsed {} operation: x={}, y={}, width={}", type, x, y, width);
FPDF_PAGE page = FPDF_LoadPage(doc_, pageIndex);
if (!page) {
spdlog::error("Failed to load page index {} for decoration", pageIndex);
return std::unexpected(EngineError::Unknown);
}
pdfengine::Path path;
if (type == "underline") {
path = DecorationBuilder::buildUnderline(x, y, width, thickness);
} else if (type == "strikeout") {
path = DecorationBuilder::buildStrikeout(x, y, width, thickness);
} else if (type == "squiggly") {
path = DecorationBuilder::buildSquiggly(x, y, width);
}
if (!path.empty()) {
const auto& segments = path.segments();
float startX = segments[0].points[0].x;
float startY = segments[0].points[0].y;
FPDF_PAGEOBJECT pathObj = FPDFPageObj_CreateNewPath(startX, startY);
bool isFill = (type != "squiggly");
bool isStroke = (type == "squiggly");
for (size_t i = 1; i < segments.size(); ++i) { // Start from 1 to skip first MoveTo
const auto& seg = segments[i];
if (seg.verb == Path::Verb::MoveTo) {
FPDFPath_MoveTo(pathObj, seg.points[0].x, seg.points[0].y);
} else if (seg.verb == Path::Verb::LineTo) {
FPDFPath_LineTo(pathObj, seg.points[0].x, seg.points[0].y);
} else if (seg.verb == Path::Verb::CubicBezierTo) {
FPDFPath_BezierTo(pathObj, seg.points[0].x, seg.points[0].y,
seg.points[1].x, seg.points[1].y,
seg.points[2].x, seg.points[2].y);
} else if (seg.verb == Path::Verb::Close) {
FPDFPath_Close(pathObj);
}
}
FPDFPath_SetDrawMode(pathObj, isFill ? FPDF_FILLMODE_ALTERNATE : FPDF_FILLMODE_NONE, isStroke);
unsigned int r = 0, g = 0, b = 0;
parseHexColor(color, r, g, b);
if (isFill) {
FPDFPageObj_SetFillColor(pathObj, r, g, b, 255);
}
if (isStroke) {
FPDFPageObj_SetStrokeColor(pathObj, r, g, b, 255);
FPDFPageObj_SetStrokeWidth(pathObj, thickness);
}
FPDFPage_InsertObject(page, pathObj);
if (!FPDFPage_GenerateContent(page)) {
spdlog::error("Failed to generate page content after adding decoration");
}
}
FPDF_ClosePage(page);
} else if (type == "redaction") {
if (!op.contains("data") || !op["data"].is_object()) {