merged code with dev

This commit is contained in:
azeeee05
2026-05-21 15:06:36 +05:30
19 changed files with 5042 additions and 45 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ node_modules/
/frontend/.vite/
# WASM artifacts
*.wasm
*.wasm.map
# Logs / misc
+2
View File
@@ -10,6 +10,8 @@ configure_file(
add_library(pdfengine STATIC
src/core/engine_info.cpp
src/parser/pdfium_loader.cpp
src/fonts/font_face.cpp
src/fonts/hb_shaper.cpp
)
add_library(pdfengine::pdfengine ALIAS pdfengine)
+75
View File
@@ -0,0 +1,75 @@
#include "font_face.hpp"
#include <iostream>
namespace pdfengine::fonts {
FontFace::FontFace()
: ft_library_(nullptr),
face_(nullptr) {
if (FT_Init_FreeType(&ft_library_)) {
std::cerr << "Failed to initialize FreeType\n";
}
}
FontFace::~FontFace() {
if (face_) {
FT_Done_Face(face_);
}
if (ft_library_) {
FT_Done_FreeType(ft_library_);
}
}
FontFace::FontFace(FontFace&& other) noexcept
: ft_library_(other.ft_library_),
face_(other.face_) {
other.ft_library_ = nullptr;
other.face_ = nullptr;
}
FontFace& FontFace::operator=(FontFace&& other) noexcept {
if (this != &other) {
if (face_) {
FT_Done_Face(face_);
}
if (ft_library_) {
FT_Done_FreeType(ft_library_);
}
ft_library_ = other.ft_library_;
face_ = other.face_;
other.ft_library_ = nullptr;
other.face_ = nullptr;
}
return *this;
}
bool FontFace::loadFromFile(const std::string& path) {
if (FT_New_Face(
ft_library_,
path.c_str(),
0,
&face_)) {
std::cerr << "Failed to load font: "
<< path << '\n';
return false;
}
// Set a default pixel size of 16px so that font coordinates and shaping
// advances are non-zero by default.
FT_Set_Pixel_Sizes(face_, 0, 16);
return true;
}
FT_Face FontFace::getFace() const {
return face_;
}
} // namespace pdfengine::fonts
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <string>
#include <ft2build.h>
#include FT_FREETYPE_H
namespace pdfengine::fonts {
class FontFace {
public:
FontFace();
~FontFace();
// FontFace is move-only to avoid double freeing FreeType resources.
FontFace(const FontFace&) = delete;
FontFace& operator=(const FontFace&) = delete;
FontFace(FontFace&& other) noexcept;
FontFace& operator=(FontFace&& other) noexcept;
bool loadFromFile(const std::string& path);
FT_Face getFace() const;
private:
FT_Library ft_library_;
FT_Face face_;
};
} // namespace pdfengine::fonts
+74
View File
@@ -0,0 +1,74 @@
#include "hb_shaper.hpp"
#include <hb.h>
#include <hb-ft.h>
namespace pdfengine::fonts {
HbShaper::HbShaper() = default;
HbShaper::~HbShaper() = default;
std::vector<ShapedGlyph> HbShaper::shapeText(const FontFace& fontFace, const std::string& text) {
std::vector<ShapedGlyph> result;
FT_Face ftFace = fontFace.getFace();
if (!ftFace) {
return result;
}
// Create a HarfBuzz font wrapper around the FreeType face.
// hb_ft_font_create_referenced increments the reference count of the FT_Face,
// making it safe even if the FontFace object changes or moves.
hb_font_t* hbFont = hb_ft_font_create_referenced(ftFace);
if (!hbFont) {
return result;
}
// Set the scale of HarfBuzz font to match the FreeType face size.
// If not set, HarfBuzz will default to using the font's design units (upem).
hb_ft_font_changed(hbFont);
// Create a text buffer.
hb_buffer_t* hbBuffer = hb_buffer_create();
if (!hbBuffer) {
hb_font_destroy(hbFont);
return result;
}
// Add text to buffer as UTF-8.
hb_buffer_add_utf8(hbBuffer, text.c_str(), static_cast<int>(text.length()), 0, -1);
// Let HarfBuzz guess direction, script, and language properties.
hb_buffer_guess_segment_properties(hbBuffer);
// Shape the text inside the buffer using the font.
hb_shape(hbFont, hbBuffer, nullptr, 0);
// Retrieve the results.
unsigned int glyphCount = 0;
hb_glyph_info_t* glyphInfos = hb_buffer_get_glyph_infos(hbBuffer, &glyphCount);
hb_glyph_position_t* glyphPositions = hb_buffer_get_glyph_positions(hbBuffer, &glyphCount);
if (glyphInfos && glyphPositions && glyphCount > 0) {
result.reserve(glyphCount);
for (unsigned int i = 0; i < glyphCount; ++i) {
ShapedGlyph sg;
sg.glyphIndex = glyphInfos[i].codepoint;
// HarfBuzz coordinates are fractional 26.6 pixels (1/64 of a pixel).
// Convert to standard double-precision float values.
sg.xAdvance = static_cast<double>(glyphPositions[i].x_advance) / 64.0;
sg.yAdvance = static_cast<double>(glyphPositions[i].y_advance) / 64.0;
sg.xOffset = static_cast<double>(glyphPositions[i].x_offset) / 64.0;
sg.yOffset = static_cast<double>(glyphPositions[i].y_offset) / 64.0;
result.push_back(sg);
}
}
// Clean up HarfBuzz resources.
hb_buffer_destroy(hbBuffer);
hb_font_destroy(hbFont);
return result;
}
} // namespace pdfengine::fonts
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "font_face.hpp"
#include <string>
#include <vector>
namespace pdfengine::fonts {
struct ShapedGlyph {
unsigned int glyphIndex;
double xAdvance;
double yAdvance;
double xOffset;
double yOffset;
};
class HbShaper {
public:
HbShaper();
~HbShaper();
HbShaper(const HbShaper&) = delete;
HbShaper& operator=(const HbShaper&) = delete;
HbShaper(HbShaper&&) noexcept = default;
HbShaper& operator=(HbShaper&&) noexcept = default;
// Shapes the input UTF-8 text using the given FontFace.
// Returns a vector of shaped glyphs.
std::vector<ShapedGlyph> shapeText(const FontFace& fontFace, const std::string& text);
};
} // namespace pdfengine::fonts
+6
View File
@@ -2,6 +2,7 @@
add_executable(pdfengine_smoke
smoke_test.cpp
fonts_test.cpp
)
target_link_libraries(pdfengine_smoke
@@ -11,6 +12,11 @@ target_link_libraries(pdfengine_smoke
GTest::gtest_main
)
target_include_directories(pdfengine_smoke
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
)
pdfengine_set_warnings(pdfengine_smoke)
pdfengine_enable_sanitizers(pdfengine_smoke)
+131
View File
@@ -0,0 +1,131 @@
#include "fonts/font_face.hpp"
#include "fonts/hb_shaper.hpp"
#include <gtest/gtest.h>
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
namespace {
std::string getSystemFontPath() {
#if defined(_WIN32)
// Common Windows fonts
std::vector<std::string> paths = {
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\consola.ttf",
"C:\\Windows\\Fonts\\tahoma.ttf"
};
#elif defined(__APPLE__)
// Common macOS fonts
std::vector<std::string> paths = {
"/Library/Fonts/Arial.ttf",
"/System/Library/Fonts/Geneva.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf"
};
#else
// Common Linux fonts
std::vector<std::string> paths = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf"
};
#endif
for (const auto& path : paths) {
if (std::filesystem::exists(path)) {
return path;
}
}
return "";
}
} // namespace
namespace pdfengine::fonts {
TEST(FontTest, FontFaceInitialization) {
FontFace face;
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceLoadNonExistentFile) {
FontFace face;
EXPECT_FALSE(face.loadFromFile("this_file_does_not_exist_12345.ttf"));
EXPECT_EQ(face.getFace(), nullptr);
}
TEST(FontTest, FontFaceMoveSemantics) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run move semantics test.";
}
FontFace face1;
ASSERT_TRUE(face1.loadFromFile(fontPath));
FT_Face rawFace = face1.getFace();
ASSERT_NE(rawFace, nullptr);
// Move construction
FontFace face2(std::move(face1));
EXPECT_EQ(face1.getFace(), nullptr);
EXPECT_EQ(face2.getFace(), rawFace);
// Move assignment
FontFace face3;
face3 = std::move(face2);
EXPECT_EQ(face2.getFace(), nullptr);
EXPECT_EQ(face3.getFace(), rawFace);
}
TEST(FontTest, HbShaperEmptyInput) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
GTEST_SKIP() << "No system font found to run empty input shaper test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
HbShaper shaper;
auto glyphs = shaper.shapeText(face, "");
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperNullFace) {
FontFace face; // Null face
HbShaper shaper;
auto glyphs = shaper.shapeText(face, "Hello");
EXPECT_TRUE(glyphs.empty());
}
TEST(FontTest, HbShaperShapeTextSuccess) {
std::string fontPath = getSystemFontPath();
if (fontPath.empty()) {
std::cout << "[ WARNING ] Skipping shape success test: no system font found." << std::endl;
GTEST_SKIP() << "No system font found to run text shaping test.";
}
FontFace face;
ASSERT_TRUE(face.loadFromFile(fontPath));
ASSERT_NE(face.getFace(), nullptr);
HbShaper shaper;
std::string testText = "Hello World!";
auto glyphs = shaper.shapeText(face, testText);
// Validate that some glyphs were shaped.
// Note that the number of glyphs doesn't strictly have to match testText.length() (e.g. ligatures),
// but for simple English it's usually 1:1.
EXPECT_FALSE(glyphs.empty());
for (const auto& g : glyphs) {
// Glyph index should be non-zero for valid glyphs (0 is usually .notdef)
// Note: some fonts might not map all characters, but Arial/DejaVu/Consolas should map ASCII.
EXPECT_GT(g.xAdvance, 0.0);
}
}
} // namespace pdfengine::fonts
+18
View File
@@ -0,0 +1,18 @@
#include <iostream>
#include <emscripten/emscripten.h>
extern "C" {
// Exported function: add two numbers
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
// Exported function: print hello message
EMSCRIPTEN_KEEPALIVE
void hello() {
std::cout << "Hello from C++ WASM!" << std::endl;
}
}
+12 -23
View File
@@ -57,6 +57,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -266,29 +267,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -875,6 +853,7 @@
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -885,6 +864,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -944,6 +924,7 @@
"integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
@@ -1174,6 +1155,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1264,6 +1246,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -1399,6 +1382,7 @@
"integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2",
@@ -2287,6 +2271,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2348,6 +2333,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2504,6 +2490,7 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2590,6 +2577,7 @@
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -2714,6 +2702,7 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1 -1
View File
@@ -31,7 +31,7 @@ function App() {
try {
const health = await gatewayService.getHealth();
setBackendHealthy(health.engine_available || true);
} catch (err) {
} catch {
setBackendHealthy(false);
}
};
+1 -1
View File
@@ -24,7 +24,7 @@ export interface RenderParams {
export interface EditOperation {
type: 'add_text' | 'delete_text' | 'draw_shape' | 'add_annotation';
pageIndex: number;
data: any;
data: unknown;
}
class GatewayService {
-4
View File
@@ -24,10 +24,6 @@ class WasmLoader {
}
try {
// In a real production setup, we load the wasm glue script:
// @ts-ignore
// const Module = await import('@/wasm/pdfengine.js');
// For Phase 0 / Gate G0a, we stub/mock the WASM loading process gracefully:
return new Promise((resolve) => {
setTimeout(() => {
this.isLoaded = true;
-1
View File
@@ -20,7 +20,6 @@ interface AnnotationLayerProps {
}
export const AnnotationLayer: React.FC<AnnotationLayerProps> = ({
pageIndex: _pageIndex,
width,
height,
zoom,
+27 -13
View File
@@ -25,6 +25,8 @@ interface PageLayout {
top: number;
}
const generateUniqueId = () => `anno_${Math.random().toString(36).substring(2, 11)}`;
export const PDFViewer: React.FC<PDFViewerProps> = ({
documentId,
totalPages,
@@ -136,32 +138,44 @@ export const PDFViewer: React.FC<PDFViewerProps> = ({
// Load the rendered SVG/Image URL for each visible page
useEffect(() => {
let active = true;
const fetchPageImages = async () => {
const newUrls = [...renderedPages];
const renderPromises = visiblePages.map(async (page) => {
if (!newUrls[page.index]) {
const missingPages = visiblePages.filter((page) => !renderedPages[page.index]);
if (missingPages.length === 0) return;
const renders = await Promise.all(
missingPages.map(async (page) => {
const url = await gatewayService.renderPage({
documentId,
pageIndex: page.index,
zoom,
rotation,
});
newUrls[page.index] = url;
}
});
return { index: page.index, url };
})
);
await Promise.all(renderPromises);
setRenderedPages(newUrls);
if (!active) return;
setRenderedPages((prev) => {
const next = [...prev];
renders.forEach(({ index, url }) => {
next[index] = url;
});
return next;
});
};
fetchPageImages();
}, [visiblePages, documentId, zoom, rotation]);
return () => {
active = false;
};
}, [visiblePages, documentId, zoom, rotation, renderedPages]);
const handleTextSelection = (text: string, bbox: Rect, _pageIndex: number) => {
const handleTextSelection = (text: string, bbox: Rect) => {
if (activeTool === 'highlight') {
const newAnno: Annotation = {
id: `anno_${Math.random().toString(36).substr(2, 9)}`,
id: generateUniqueId(),
type: 'highlight',
bbox: {
x: bbox.x / zoom,
@@ -226,7 +240,7 @@ export const PDFViewer: React.FC<PDFViewerProps> = ({
width={page.width}
height={page.height}
zoom={zoom}
onTextSelected={(text, bbox) => handleTextSelection(text, bbox, page.index)}
onTextSelected={(text, bbox) => handleTextSelection(text, bbox)}
/>
{/* signature/ink overlay tool layer */}
-1
View File
@@ -13,7 +13,6 @@ export const SelectionLayer: React.FC<SelectionLayerProps> = ({
pageIndex,
width,
height,
zoom: _zoom,
onTextSelected,
}) => {
const [dragStart, setDragStart] = useState<Point | null>(null);
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WASM Hello World</title>
</head>
<body>
<h1>WASM Hello World</h1>
<p>Open browser console (F12) to see output.</p>
<!-- Load generated Emscripten JS -->
<script src="wasm_hello.js"></script>
<script>
// Wait until WASM runtime is ready
Module.onRuntimeInitialized = () => {
console.log("WASM Runtime Initialized");
// Call C++ hello() function
Module.ccall(
'hello', // C++ function name
null, // return type
[], // argument types
[] // arguments
);
// Call C++ add() function
const result = Module.ccall(
'add', // function name
'number', // return type
['number', 'number'], // argument types
[5, 7] // arguments
);
console.log("Result =", result);
};
</script>
</body>
</html>
+4583
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.