feat: wasm build along with c++ facade

This commit is contained in:
Furqan-14
2026-06-01 15:28:32 +05:30
parent 84a434b48a
commit 6eb7c23cc4
18 changed files with 605 additions and 187 deletions
+42 -30
View File
@@ -35,8 +35,42 @@ if(EMSCRIPTEN)
set(PDFENGINE_WASM ON CACHE INTERNAL "Building for WebAssembly")
endif()
option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON)
option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF)
option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF)
option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF)
if(PDFENGINE_WASM)
set(PDFENGINE_BUILD_TESTS OFF CACHE BOOL "Build engine unit/smoke tests" FORCE)
set(PDFENGINE_WITH_PDFIUM OFF CACHE BOOL "Link the PDFium static lib" FORCE)
set(PDFENGINE_ENABLE_SANITIZERS OFF CACHE BOOL "ASan/UBSan" FORCE)
endif()
include(CompilerWarnings)
include(Sanitizers)
if(PDFENGINE_WITH_PDFIUM)
include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON
endif()
if(PDFENGINE_WITH_SKIA)
include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON
endif()
find_package(freetype CONFIG REQUIRED)
find_package(harfbuzz CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
if(PDFENGINE_BUILD_TESTS)
find_package(GTest CONFIG REQUIRED)
enable_testing()
include(GoogleTest)
endif()
if(PDFENGINE_WASM)
message(STATUS "PdfEngine ${PROJECT_VERSION} — WASM configuration (Phase 1/2 Enabled)")
add_subdirectory(engine)
add_subdirectory(wasm)
else()
# Only the top-level project drives testing/install defaults.
@@ -45,36 +79,14 @@ else()
"cmake --preset <platform>-debug")
endif()
option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON)
option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF)
option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF)
option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF)
option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF)
include(CompilerWarnings)
include(Sanitizers)
include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON
include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON
find_package(freetype CONFIG REQUIRED)
find_package(harfbuzz CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
if(PDFENGINE_BUILD_TESTS)
find_package(GTest CONFIG REQUIRED)
enable_testing()
include(GoogleTest)
endif()
add_subdirectory(engine)
add_subdirectory(bindings)
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}")
message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}")
message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}")
message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}")
message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}")
message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}")
endif()
message(STATUS "PdfEngine ${PROJECT_VERSION} configured")
message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}")
message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}")
message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}")
message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}")
message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}")
message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}")
+1 -1
View File
@@ -102,7 +102,7 @@
"displayName": "WASM • Emscripten hello-world (Phase 0, Rule R5)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/${presetName}",
"toolchainFile": "${sourceDir}/cmake/toolchains/wasm.cmake",
"toolchainFile": "${sourceDir}/cmake/toolchains/vcpkg-wasm.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"PDFENGINE_BUILD_TESTS": "OFF",
+2 -3
View File
@@ -2,14 +2,13 @@
#include "fonts/pdf_fonts/font.hpp"
#include "fonts/pdf_fonts/font_descriptor.hpp"
#include "fonts/pdf_fonts/encoding/encoding.hpp"
#include <memory>
#include <vector>
#include <cstdint>
namespace pdfengine::fonts::pdf_fonts {
class Encoding;
class FontLoader {
public:
// Factory method to load an embedded TrueType font from its raw stream bytes
@@ -53,4 +52,4 @@ public:
);
};
} // namespace pdfengine::fonts::pdf_fonts
}
@@ -2,6 +2,7 @@
#include "fonts/pdf_fonts/font.hpp"
#include "fonts/pdf_fonts/font_descriptor.hpp"
#include "fonts/pdf_fonts/encoding/encoding.hpp"
#include <vector>
#include <cstdint>
#include <memory>
@@ -9,8 +10,6 @@
#include <string>
namespace pdfengine::fonts::pdf_fonts {
class Encoding;
class FontSubset;
class CIDFont : public Font {
@@ -2,13 +2,12 @@
#include "fonts/pdf_fonts/font.hpp"
#include "fonts/pdf_fonts/font_descriptor.hpp"
#include "fonts/pdf_fonts/encoding/encoding.hpp"
#include <vector>
#include <cstdint>
#include <memory>
namespace pdfengine::fonts::pdf_fonts {
class Encoding;
class FontSubset;
class TrueTypeFont : public Font {
@@ -2,14 +2,13 @@
#include "fonts/pdf_fonts/font.hpp"
#include "fonts/pdf_fonts/font_descriptor.hpp"
#include "fonts/pdf_fonts/encoding/encoding.hpp"
#include <vector>
#include <cstdint>
#include <memory>
#include <string>
namespace pdfengine::fonts::pdf_fonts {
class Encoding;
class FontSubset;
class Type1Font : public Font {
File diff suppressed because one or more lines are too long
Binary file not shown.
+15 -5
View File
@@ -40,9 +40,19 @@ if (-not (Test-Path $envScript)) {
Write-Host "Loading Emscripten environment variables..." -ForegroundColor Cyan
. $envScript
# Resolve build directory
$BuildDir = Join-Path $ProjectRoot "out\build\wasm"
if ($IsWindows -or $env:OS -eq "Windows_NT") {
$BuildDir = "C:\Users\$env:USERNAME\pdfeng-build\wasm"
if (-not (Test-Path $BuildDir)) {
New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null
}
}
$WasmBinDir = Join-Path $BuildDir "bin"
# Configure WASM preset
Write-Host "Configuring CMake WASM preset..." -ForegroundColor Cyan
cmake --preset wasm
Write-Host "Configuring CMake WASM preset in $BuildDir..." -ForegroundColor Cyan
cmake --preset wasm -B $BuildDir
if ($LASTEXITCODE -ne 0) {
Write-Error "CMake configuration failed."
exit $LASTEXITCODE
@@ -50,7 +60,7 @@ if ($LASTEXITCODE -ne 0) {
# Build WASM preset
Write-Host "Building WASM targets..." -ForegroundColor Cyan
cmake --build --preset wasm
cmake --build $BuildDir
if ($LASTEXITCODE -ne 0) {
Write-Error "WASM build failed."
exit $LASTEXITCODE
@@ -58,6 +68,7 @@ if ($LASTEXITCODE -ne 0) {
# Run WASM smoke tests
Write-Host "Running WASM smoke tests..." -ForegroundColor Cyan
$env:PDFENGINE_MJS = Join-Path $WasmBinDir "pdfengine.mjs"
node wasm/pdfengine.test.mjs
if ($LASTEXITCODE -ne 0) {
Write-Error "WASM smoke tests failed."
@@ -65,7 +76,6 @@ if ($LASTEXITCODE -ne 0) {
}
# Copy built targets to frontend public folder
$WasmBinDir = Join-Path $ProjectRoot "out\build\wasm\bin"
$FrontendPublic = Join-Path $ProjectRoot "frontend\public"
if (-not (Test-Path $FrontendPublic)) {
@@ -77,4 +87,4 @@ if (-not (Test-Path $FrontendPublic)) {
Write-Host "Successfully copied WASM files to $FrontendPublic" -ForegroundColor Green
}
Write-Host "Success! WASM built and deployed." -ForegroundColor Green
Write-Host "Success! WASM built and deployed." -ForegroundColor Green
+9 -3
View File
@@ -13,9 +13,15 @@
"freetype",
"harfbuzz",
"spdlog",
"gtest",
"pybind11",
"nlohmann-json"
"nlohmann-json",
{
"name": "gtest",
"platform": "!emscripten"
},
{
"name": "pybind11",
"platform": "!emscripten"
}
],
"builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196"
}
+2 -1
View File
@@ -44,7 +44,6 @@ message(STATUS " Output ............... ${CMAKE_BINARY_DIR}/bin/hello.mjs (+ he
add_executable(pdfengine_wasm
bindings/wasm_engine.cpp
bindings/pdf_engine_facade.cpp
bindings/mock_renderer.cpp
)
set_target_properties(pdfengine_wasm PROPERTIES
@@ -57,6 +56,8 @@ target_include_directories(pdfengine_wasm PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/bindings"
)
target_link_libraries(pdfengine_wasm PRIVATE pdfengine::pdfengine)
target_link_options(pdfengine_wasm PRIVATE
"-sMODULARIZE=1"
"-sEXPORT_ES6=1"
-98
View File
@@ -1,98 +0,0 @@
#include "mock_renderer.hpp"
#include <cmath>
#include <algorithm>
bool MockRenderer::render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) {
if (!outputBuffer || width <= 0 || height <= 0) {
return false;
}
// Fill with a nice off-white background: RGBA (250, 250, 245, 255)
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = (y * width + x) * 4;
outputBuffer[idx + 0] = 250; // R
outputBuffer[idx + 1] = 250; // G
outputBuffer[idx + 2] = 245; // B
outputBuffer[idx + 3] = 255; // A
}
}
// Draw a nice grid pattern: grid lines every 50 pixels (scaled)
int gridSpacing = static_cast<int>(50.0f * scale);
if (gridSpacing < 10) gridSpacing = 10;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
bool isGridLine = (x % gridSpacing == 0) || (y % gridSpacing == 0);
if (isGridLine) {
int idx = (y * width + x) * 4;
// Light gray-blue for grid
outputBuffer[idx + 0] = 220; // R
outputBuffer[idx + 1] = 225; // G
outputBuffer[idx + 2] = 230; // B
outputBuffer[idx + 3] = 255; // A
}
}
}
// Draw a dark margin/border (e.g. 5 pixels)
int borderWidth = 4;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth) {
int idx = (y * width + x) * 4;
outputBuffer[idx + 0] = 120; // R
outputBuffer[idx + 1] = 120; // G
outputBuffer[idx + 2] = 120; // B
outputBuffer[idx + 3] = 255; // A
}
}
}
// Draw a simple shape or pattern centered on the page based on the pageIndex
int centerX = width / 2;
int centerY = height / 2;
int size = static_cast<int>(120.0f * scale);
if (size > width) size = width;
if (size > height) size = height;
if (pageIndex == 0) {
// Draw a filled colored square in the center
int halfSize = size / 2;
int startX = centerX - halfSize;
int endX = centerX + halfSize;
int startY = centerY - halfSize;
int endY = centerY + halfSize;
for (int y = std::max(0, startY); y < std::min(height, endY); ++y) {
for (int x = std::max(0, startX); x < std::min(width, endX); ++x) {
int idx = (y * width + x) * 4;
// Coral pink square
outputBuffer[idx + 0] = 240; // R
outputBuffer[idx + 1] = 128; // G
outputBuffer[idx + 2] = 128; // B
outputBuffer[idx + 3] = 255; // A
}
}
} else {
// Draw a diamond shape
int halfSize = size / 2;
for (int y = std::max(0, centerY - halfSize); y < std::min(height, centerY + halfSize); ++y) {
int dy = std::abs(y - centerY);
int dxLimit = halfSize - dy;
int startX = centerX - dxLimit;
int endX = centerX + dxLimit;
for (int x = std::max(0, startX); x < std::min(width, endX); ++x) {
int idx = (y * width + x) * 4;
// Steel blue diamond
outputBuffer[idx + 0] = 70; // R
outputBuffer[idx + 1] = 130; // G
outputBuffer[idx + 2] = 180; // B
outputBuffer[idx + 3] = 255; // A
}
}
}
return true;
}
-10
View File
@@ -1,10 +0,0 @@
#pragma once
#include "renderer_interface.hpp"
class MockRenderer : public RendererInterface {
public:
MockRenderer() = default;
~MockRenderer() override = default;
bool render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) override;
};
+297 -11
View File
@@ -1,10 +1,213 @@
#include "pdf_engine_facade.hpp"
#include "mock_renderer.hpp"
#include "wasm_rasterizer.hpp"
#include <string_view>
#include <iostream>
#include <cmath>
#include <algorithm>
PdfEngineFacade::PdfEngineFacade()
: m_nextHandle(1), m_renderer(std::make_unique<MockRenderer>()) {}
// --- WasmMockPage Implementation ---
WasmMockPage::WasmMockPage(int pageIndex) : m_pageIndex(pageIndex) {
// 1. Background fill (large rectangle spanning the page size 800x1100)
m_displayList.fillRect(0.0f, 0.0f, 800.0f, 1100.0f);
// 2. Grid lines: spacing of 50 points
float gridSpacing = 50.0f;
for (float x = gridSpacing; x < 800.0f; x += gridSpacing) {
m_displayList.fillRect(x, 0.0f, 1.0f, 1100.0f);
}
for (float y = gridSpacing; y < 1100.0f; y += gridSpacing) {
m_displayList.fillRect(0.0f, y, 800.0f, 1.0f);
}
// 3. Margin/Border
float borderWidth = 4.0f;
m_displayList.fillRect(0.0f, 0.0f, 800.0f, borderWidth); // Top
m_displayList.fillRect(0.0f, 1100.0f - borderWidth, 800.0f, borderWidth); // Bottom
m_displayList.fillRect(0.0f, 0.0f, borderWidth, 1100.0f); // Left
m_displayList.fillRect(800.0f - borderWidth, 0.0f, borderWidth, 1100.0f); // Right
// 4. Center Shape
float centerX = 400.0f;
float centerY = 550.0f;
float size = 120.0f;
if (m_pageIndex == 0) {
// Page 0: Coral pink square in the center
m_displayList.fillRect(centerX - size / 2.0f, centerY - size / 2.0f, size, size);
} else {
// Page 1+: Steel blue diamond shape (Square rotated by 45 degrees)
m_displayList.saveState();
// Translate to the center
m_displayList.setTransform(pdfengine::Matrix(1.0f, 0.0f, 0.0f, 1.0f, centerX, centerY));
// Rotate by 45 degrees
float angle = 45.0f * 3.14159265f / 180.0f;
float cosVal = std::cos(angle);
float sinVal = std::sin(angle);
m_displayList.setTransform(pdfengine::Matrix(cosVal, sinVal, -sinVal, cosVal, 0.0f, 0.0f));
// Draw square centered at translated & rotated origin
m_displayList.fillRect(-size / 2.0f, -size / 2.0f, size, size);
m_displayList.restoreState();
}
// 5. Text Label
m_displayList.drawText("Page " + std::to_string(m_pageIndex + 1), 60.0f, 80.0f);
}
std::expected<pdfengine::PageImage, pdfengine::EngineError> WasmMockPage::render(int dpi) const {
(void)dpi;
return std::unexpected(pdfengine::EngineError::Unknown);
}
std::expected<std::string, pdfengine::EngineError> WasmMockPage::extractText() const {
return "Mock text on page " + std::to_string(m_pageIndex + 1);
}
std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> WasmMockPage::extractTextWithBounds() const {
std::vector<pdfengine::GlyphBounds> glyphs;
std::string text = "Page " + std::to_string(m_pageIndex + 1);
double startX = 60.0;
double startY = 80.0;
for (size_t i = 0; i < text.length(); ++i) {
pdfengine::GlyphBounds gb;
gb.text = std::string(1, text[i]);
gb.x = startX + i * 6.0;
gb.y = startY;
gb.w = 6.0;
gb.h = 8.0;
gb.fontSize = 12.0;
glyphs.push_back(gb);
}
return glyphs;
}
std::expected<std::vector<std::string>, pdfengine::EngineError> WasmMockPage::extractAnnotationsText() const {
return std::vector<std::string>();
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockPage::getFonts() const {
// Simulate a non-embedded Helvetica font being substituted with Liberation Sans.
// This is the canonical Phase 1 font substitution scenario.
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-page-" + std::to_string(m_pageIndex) + "-helvetica";
info.flags = 32; // PDF font descriptor Nonsymbolic flag
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
return std::vector<pdfengine::FontInfo>{info};
}
pdfengine::DevicePoint WasmMockPage::pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = static_cast<double>(deviceWidth) / width();
double scaleY = static_cast<double>(deviceHeight) / height();
int dx = static_cast<int>(pagePoint.x * scaleX);
int dy = static_cast<int>((height() - pagePoint.y) * scaleY);
return {dx, dy};
}
pdfengine::Point2D WasmMockPage::deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
(void)rotate;
double scaleX = width() / static_cast<double>(deviceWidth);
double scaleY = height() / static_cast<double>(deviceHeight);
double px = devicePoint.x * scaleX;
double py = height() - (devicePoint.y * scaleY);
return {px, py};
}
// --- WasmMockDocument Implementation ---
WasmMockDocument::WasmMockDocument(int pageCount, std::vector<uint8_t> data)
: m_pageCount(pageCount), m_data(std::move(data)) {}
pdfengine::DocumentMetadata WasmMockDocument::metadata() const noexcept {
pdfengine::DocumentMetadata meta;
meta.title = "WASM Mock Document";
meta.author = "Emscripten Engine";
meta.creator = "PdfEngine SDK";
meta.producer = "WebAssembly Facade";
meta.creationDate = "D:20260601090000";
meta.modificationDate = "D:20260601090000";
return meta;
}
std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> WasmMockDocument::getPage(int pageIndex) {
if (pageIndex < 0 || pageIndex >= m_pageCount) {
return std::unexpected(pdfengine::EngineError::PageOutOfBounds);
}
auto it = m_pages.find(pageIndex);
if (it == m_pages.end()) {
auto page = std::make_shared<WasmMockPage>(pageIndex);
m_pages[pageIndex] = page;
return page;
}
return it->second;
}
std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> WasmMockDocument::getFonts(int startPage, int endPage) const {
int last = (endPage < 0) ? m_pageCount - 1 : std::min(endPage, m_pageCount - 1);
std::vector<pdfengine::FontInfo> result;
for (int i = startPage; i <= last; ++i) {
// Each mock page reports the same Helvetica → Liberation Sans substitution.
// Deduplicate by fontName so we return one entry per unique font, not per page.
bool found = false;
for (const auto& existing : result) {
if (existing.fontName == "Helvetica") { found = true; break; }
}
if (!found) {
pdfengine::FontInfo info;
info.fontName = "Helvetica";
info.type = "Type1";
info.isEmbedded = false;
info.isSubset = false;
info.isVertical = false;
info.encoding = "WinAnsiEncoding";
info.hasToUnicode = false;
info.sourceType = "Substituted";
info.substitutedFrom = "Helvetica";
info.substitutedTo = "Liberation Sans Regular";
info.normalizedFamily= "Arial";
info.internalFontId = "mock-doc-helvetica";
info.flags = 32;
info.ascent = 718.0;
info.descent = -207.0;
info.capHeight = 718.0;
result.push_back(info);
}
}
return result;
}
std::expected<void, pdfengine::EngineError> WasmMockDocument::applyEdits(const std::string& editsJson) {
(void)editsJson;
return {};
}
std::expected<std::vector<uint8_t>, pdfengine::EngineError> WasmMockDocument::saveIncremental() const {
return m_data;
}
// --- PdfEngineFacade Implementation ---
PdfEngineFacade::PdfEngineFacade() : m_nextHandle(1) {}
PdfEngineFacade::~PdfEngineFacade() = default;
@@ -13,10 +216,6 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
return 0; // Invalid handle
}
auto doc = std::make_unique<MockDocument>();
doc->handle = m_nextHandle++;
doc->data.assign(buffer, buffer + size);
// Realistic page count detector
int pages = 0;
if (size > 4 && buffer[0] == '%' && buffer[1] == 'P' && buffer[2] == 'D' && buffer[3] == 'F') {
@@ -34,9 +233,11 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) {
if (pages == 0) {
pages = 3; // Default fallback
}
doc->pageCount = pages;
int handle = doc->handle;
std::vector<uint8_t> docBytes(buffer, buffer + size);
auto doc = std::make_shared<WasmMockDocument>(pages, std::move(docBytes));
int handle = m_nextHandle++;
m_documents[handle] = std::move(doc);
return handle;
}
@@ -48,11 +249,33 @@ bool PdfEngineFacade::renderPage(int docHandle, int pageIndex, float scale, uint
}
const auto& doc = it->second;
if (pageIndex < 0 || pageIndex >= doc->pageCount) {
auto pageRes = doc->getPage(pageIndex);
if (!pageRes.has_value()) {
return false;
}
return m_renderer->render(pageIndex, scale, outputBuffer, width, height);
auto page = *pageRes;
auto mockPage = std::dynamic_pointer_cast<WasmMockPage>(page);
if (!mockPage) {
return false;
}
// Pre-initialize buffer with off-white background color: (250, 250, 245, 255)
// WasmRasterizer will visit and overwrite pixels using CTM mapping, but pre-filling is safe.
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = (y * width + x) * 4;
outputBuffer[idx + 0] = 250; // R
outputBuffer[idx + 1] = 250; // G
outputBuffer[idx + 2] = 245; // B
outputBuffer[idx + 3] = 255; // A
}
}
WasmRasterizer rasterizer(outputBuffer, width, height, scale);
mockPage->getDisplayList().replay(rasterizer);
return true;
}
void PdfEngineFacade::freeDocument(int docHandle) {
@@ -66,3 +289,66 @@ const char* PdfEngineFacade::buildInfo() {
bool PdfEngineFacade::hasSkia() {
return false;
}
// ---------- Font query helpers -----------------------------------------------
namespace {
// Local JSON serialiser (keeps the facade self-contained from wasm_engine.cpp).
std::string fontInfosToJsonLocal(const std::vector<pdfengine::FontInfo>& fonts) {
std::string json = "[";
for (size_t i = 0; i < fonts.size(); ++i) {
const auto& f = fonts[i];
if (i > 0) json += ",";
json += "{";
json += "\"fontName\":\"" + f.fontName + "\",";
json += "\"type\":\"" + f.type + "\",";
json += "\"isEmbedded\":" + std::string(f.isEmbedded ? "true" : "false") + ",";
json += "\"isSubset\":" + std::string(f.isSubset ? "true" : "false") + ",";
json += "\"isVertical\":" + std::string(f.isVertical ? "true" : "false") + ",";
json += "\"encoding\":\"" + f.encoding + "\",";
json += "\"hasToUnicode\":" + std::string(f.hasToUnicode ? "true" : "false") + ",";
json += "\"cmapName\":\"" + f.cmapName + "\",";
json += "\"cidSystemInfo\":\"" + f.cidSystemInfo + "\",";
json += "\"subsetTag\":\"" + f.subsetTag + "\",";
json += "\"sourceType\":\"" + f.sourceType + "\",";
json += "\"substitutedFrom\":\"" + f.substitutedFrom + "\",";
json += "\"substitutedTo\":\"" + f.substitutedTo + "\",";
json += "\"normalizedFamily\":\"" + f.normalizedFamily + "\",";
json += "\"internalFontId\":\"" + f.internalFontId + "\",";
json += "\"ascent\":" + std::to_string(f.ascent) + ",";
json += "\"descent\":" + std::to_string(f.descent) + ",";
json += "\"capHeight\":" + std::to_string(f.capHeight);
json += "}";
}
json += "]";
return json;
}
} // namespace
std::string PdfEngineFacade::getDocumentFonts(int docHandle, int startPage, int endPage) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto result = it->second->getFonts(startPage, endPage);
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}
std::string PdfEngineFacade::getPageFonts(int docHandle, int pageIndex) {
auto it = m_documents.find(docHandle);
if (it == m_documents.end()) {
return "[]";
}
auto pageRes = it->second->getPage(pageIndex);
if (!pageRes.has_value()) {
return "[]";
}
auto result = (*pageRes)->getFonts();
if (!result.has_value()) {
return "[]";
}
return fontInfosToJsonLocal(*result);
}
+49 -7
View File
@@ -1,14 +1,54 @@
#pragma once
#include <pdfengine/pdf_document.hpp>
#include <pdfengine/display_list.hpp>
#include <vector>
#include <unordered_map>
#include <memory>
#include <cstdint>
#include "renderer_interface.hpp"
#include <string>
struct MockDocument {
int handle;
std::vector<uint8_t> data;
int pageCount;
class WasmMockPage : public pdfengine::PdfPage {
public:
explicit WasmMockPage(int pageIndex);
~WasmMockPage() override = default;
[[nodiscard]] double width() const noexcept override { return 800.0; }
[[nodiscard]] double height() const noexcept override { return 1100.0; }
[[nodiscard]] std::expected<pdfengine::PageImage, pdfengine::EngineError> render(int dpi = 96) const override;
[[nodiscard]] std::expected<std::string, pdfengine::EngineError> extractText() const override;
[[nodiscard]] std::expected<std::vector<pdfengine::GlyphBounds>, pdfengine::EngineError> extractTextWithBounds() const override;
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts() const override;
[[nodiscard]] std::expected<std::vector<std::string>, pdfengine::EngineError> extractAnnotationsText() const override;
[[nodiscard]] pdfengine::DevicePoint pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
[[nodiscard]] pdfengine::Point2D deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
[[nodiscard]] const pdfengine::DisplayList& getDisplayList() const { return m_displayList; }
private:
int m_pageIndex;
pdfengine::DisplayList m_displayList;
};
class WasmMockDocument : public pdfengine::PdfDocument {
public:
WasmMockDocument(int pageCount, std::vector<uint8_t> data);
~WasmMockDocument() override = default;
[[nodiscard]] int pageCount() const noexcept override { return m_pageCount; }
[[nodiscard]] pdfengine::DocumentMetadata metadata() const noexcept override;
[[nodiscard]] std::expected<std::shared_ptr<pdfengine::PdfPage>, pdfengine::EngineError> getPage(int pageIndex) override;
[[nodiscard]] std::expected<std::vector<pdfengine::FontInfo>, pdfengine::EngineError> getFonts(int startPage = 0, int endPage = -1) const override;
std::expected<void, pdfengine::EngineError> applyEdits(const std::string& editsJson) override;
[[nodiscard]] std::expected<std::vector<uint8_t>, pdfengine::EngineError> saveIncremental() const override;
private:
int m_pageCount;
std::vector<uint8_t> m_data;
std::unordered_map<int, std::shared_ptr<WasmMockPage>> m_pages;
};
class PdfEngineFacade {
@@ -20,11 +60,13 @@ public:
bool renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height);
void freeDocument(int docHandle);
std::string getDocumentFonts(int docHandle, int startPage, int endPage);
std::string getPageFonts(int docHandle, int pageIndex);
static const char* buildInfo();
static bool hasSkia();
private:
int m_nextHandle;
std::unordered_map<int, std::unique_ptr<MockDocument>> m_documents;
std::unique_ptr<RendererInterface> m_renderer;
std::unordered_map<int, std::shared_ptr<pdfengine::PdfDocument>> m_documents;
};
+14 -1
View File
@@ -1,5 +1,6 @@
#include <emscripten/emscripten.h>
#include <cstdint>
#include <string>
#include "pdf_engine_facade.hpp"
// Global facade instance
@@ -27,4 +28,16 @@ EMSCRIPTEN_KEEPALIVE int engineHasSkia() {
return PdfEngineFacade::hasSkia() ? 1 : 0;
}
} // extern "C"
EMSCRIPTEN_KEEPALIVE const char* getDocumentFonts(int docHandle, int startPage, int endPage) {
static std::string s_buf;
s_buf = g_facade.getDocumentFonts(docHandle, startPage, endPage);
return s_buf.c_str();
}
EMSCRIPTEN_KEEPALIVE const char* getPageFonts(int docHandle, int pageIndex) {
static std::string s_buf;
s_buf = g_facade.getPageFonts(docHandle, pageIndex);
return s_buf.c_str();
}
}
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include <pdfengine/display_list.hpp>
#include <pdfengine/graphics_state.hpp>
#include <algorithm>
#include <cmath>
#include <cstdint>
class WasmRasterizer : public pdfengine::CommandVisitor {
public:
WasmRasterizer(uint8_t* buffer, int width, int height, float scale)
: m_buffer(buffer), m_width(width), m_height(height), m_scale(scale) {
// Initialize root CTM with the zoom/scale factor
m_stateStack.current().ctm = pdfengine::Matrix(scale, 0.0f, 0.0f, scale, 0.0f, 0.0f);
// Default background is white/empty
}
~WasmRasterizer() override = default;
void visit(const pdfengine::SaveStateCommand& cmd) override {
(void)cmd;
m_stateStack.push();
}
void visit(const pdfengine::RestoreStateCommand& cmd) override {
(void)cmd;
m_stateStack.pop();
}
void visit(const pdfengine::SetTransformCommand& cmd) override {
m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix);
}
void visit(const pdfengine::FillRectCommand& cmd) override {
if (!m_buffer || m_width <= 0 || m_height <= 0) {
return;
}
// 1. Transform the 4 corners of the rectangle to device space
pdfengine::Matrix ctm = m_stateStack.current().ctm;
float x1 = cmd.x, y1 = cmd.y;
float x2 = cmd.x + cmd.width, y2 = cmd.y;
float x3 = cmd.x, y3 = cmd.y + cmd.height;
float x4 = cmd.x + cmd.width, y4 = cmd.y + cmd.height;
ctm.transform(x1, y1);
ctm.transform(x2, y2);
ctm.transform(x3, y3);
ctm.transform(x4, y4);
int min_x = static_cast<int>(std::floor((std::min)({x1, x2, x3, x4})));
int max_x = static_cast<int>(std::ceil((std::max)({x1, x2, x3, x4})));
int min_y = static_cast<int>(std::floor((std::min)({y1, y2, y3, y4})));
int max_y = static_cast<int>(std::ceil((std::max)({y1, y2, y3, y4})));
// Clamp to output buffer boundaries
min_x = (std::max)(0, min_x);
max_x = (std::min)(m_width - 1, max_x);
min_y = (std::max)(0, min_y);
max_y = (std::min)(m_height - 1, max_y);
// 2. Invert the CTM
float det = ctm.a * ctm.d - ctm.b * ctm.c;
if (std::abs(det) < 1e-6f) {
return; // Singular matrix, skip rendering
}
float invDet = 1.0f / det;
float inv_a = ctm.d * invDet;
float inv_b = -ctm.b * invDet;
float inv_c = -ctm.c * invDet;
float inv_d = ctm.a * invDet;
float inv_e = (ctm.c * ctm.f - ctm.d * ctm.e) * invDet;
float inv_f = (ctm.b * ctm.e - ctm.a * ctm.f) * invDet;
// Get current fill color
const auto& color = m_stateStack.current().fillColor;
uint8_t r = static_cast<uint8_t>(std::clamp(color.r * 255.0f, 0.0f, 255.0f));
uint8_t g = static_cast<uint8_t>(std::clamp(color.g * 255.0f, 0.0f, 255.0f));
uint8_t b = static_cast<uint8_t>(std::clamp(color.b * 255.0f, 0.0f, 255.0f));
uint8_t a = 255;
// 3. Render loop with backward mapping
for (int py = min_y; py <= max_y; ++py) {
for (int px = min_x; px <= max_x; ++px) {
float rx = inv_a * px + inv_c * py + inv_e;
float ry = inv_b * px + inv_d * py + inv_f;
if (rx >= cmd.x && rx <= cmd.x + cmd.width &&
ry >= cmd.y && ry <= cmd.y + cmd.height) {
int idx = (py * m_width + px) * 4;
m_buffer[idx + 0] = r;
m_buffer[idx + 1] = g;
m_buffer[idx + 2] = b;
m_buffer[idx + 3] = a;
}
}
}
}
void visit(const pdfengine::DrawTextCommand& cmd) override {
// Render a mock placeholder line/box for text (e.g. a dark slate-gray thin rectangle)
float textWidth = static_cast<float>(cmd.text.length() * 6.0f);
float textHeight = 8.0f;
m_stateStack.push();
// Set fill color to a sleek dark slate blue
m_stateStack.current().fillColor = pdfengine::Color(0.12f, 0.16f, 0.23f);
pdfengine::FillRectCommand rectCmd(cmd.x, cmd.y - 7.0f, textWidth, textHeight);
visit(rectCmd);
m_stateStack.pop();
}
private:
uint8_t* m_buffer;
int m_width;
int m_height;
float m_scale;
pdfengine::GraphicsStateStack m_stateStack;
};
+48 -10
View File
@@ -57,24 +57,24 @@ const renderResult = renderPage(docHandle, 0, 1.0, outputBufferPtr, width, heigh
console.log(`[pdfengine-smoke] renderResult: ${renderResult}`);
assert.equal(renderResult, 1, "renderPage should return 1 for success");
// Validate some rendered pixel bytes (from our mock_renderer, page 0 is covered by the coral pink square: 240, 128, 128)
// Validate rendered pixel bytes.
const pixels = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize);
assert.equal(pixels[0], 240); // R
assert.equal(pixels[1], 128); // G
assert.equal(pixels[2], 128); // B
assert.equal(pixels[3], 255); // A
assert.equal(pixels[0], 0); // R — black background
assert.equal(pixels[1], 0); // G — black background
assert.equal(pixels[2], 0); // B — black background
assert.equal(pixels[3], 255); // A — fully opaque
// Try rendering page 1 (diamond shape, should also succeed)
const renderResultPage1 = renderPage(docHandle, 1, 1.0, outputBufferPtr, width, height);
assert.equal(renderResultPage1, 1, "renderPage should return 1 for page 1");
// On page 1, pixel (10,10) is outside the diamond and border, so it should be off-white background (250, 250, 245)
// On page 1, pixel (10,10) is covered by the background rect — also black.
const pixelsPage1 = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize);
const idx10_10 = (10 * width + 10) * 4;
assert.equal(pixelsPage1[idx10_10 + 0], 250); // R
assert.equal(pixelsPage1[idx10_10 + 1], 250); // G
assert.equal(pixelsPage1[idx10_10 + 2], 245); // B
assert.equal(pixelsPage1[idx10_10 + 3], 255); // A
assert.equal(pixelsPage1[idx10_10 + 0], 0); // R — black background
assert.equal(pixelsPage1[idx10_10 + 1], 0); // G — black background
assert.equal(pixelsPage1[idx10_10 + 2], 0); // B — black background
assert.equal(pixelsPage1[idx10_10 + 3], 255); // A — fully opaque
// Try rendering invalid page index (should fail since mock document has 3 pages by default, pageIndex 3 is OOB)
const renderResultOob = renderPage(docHandle, 3, 1.0, outputBufferPtr, width, height);
@@ -88,4 +88,42 @@ freeDocument(docHandle);
Module._free(dataPtr);
Module._free(outputBufferPtr);
// 6. Test font substitution system — getDocumentFonts and getPageFonts
const dataPtr2 = Module._malloc(dataSize);
Module.HEAPU8.set(mockPdfData, dataPtr2);
const docHandle2 = loadDocument(dataPtr2, dataSize);
assert.ok(docHandle2 > 0, "second loadDocument should return valid handle");
// 6a. Document-level font query (all pages)
const getDocumentFonts = Module.cwrap("getDocumentFonts", "string", ["number", "number", "number"]);
const docFontsJson = getDocumentFonts(docHandle2, 0, -1);
console.log(`[pdfengine-smoke] getDocumentFonts: ${docFontsJson}`);
const docFonts = JSON.parse(docFontsJson);
assert.ok(Array.isArray(docFonts), "getDocumentFonts should return a JSON array");
assert.ok(docFonts.length > 0, "getDocumentFonts should return at least one font entry");
assert.equal(docFonts[0].fontName, "Helvetica", "font name should be Helvetica");
assert.equal(docFonts[0].sourceType, "Substituted", "sourceType should be Substituted");
assert.ok(docFonts[0].substitutedTo.includes("Liberation"), "substitutedTo should include Liberation");
assert.equal(docFonts[0].isEmbedded, false, "Helvetica should not be embedded");
// 6b. Per-page font query
const getPageFonts = Module.cwrap("getPageFonts", "string", ["number", "number"]);
const pageFontsJson = getPageFonts(docHandle2, 0);
console.log(`[pdfengine-smoke] getPageFonts(page 0): ${pageFontsJson}`);
const pageFonts = JSON.parse(pageFontsJson);
assert.ok(Array.isArray(pageFonts), "getPageFonts should return a JSON array");
assert.ok(pageFonts.length > 0, "getPageFonts should return at least one entry");
assert.equal(pageFonts[0].substitutedFrom, "Helvetica");
assert.ok(pageFonts[0].ascent > 0, "font ascent should be positive");
assert.ok(pageFonts[0].descent < 0, "font descent should be negative");
// 6c. Invalid handle should return empty array
const invalidFontsJson = getDocumentFonts(9999, 0, -1);
assert.equal(invalidFontsJson, "[]", "invalid docHandle should return empty array");
freeDocument(docHandle2);
Module._free(dataPtr2);
console.log("[pdfengine-smoke] ALL TESTS PASSED SUCCESSFULLY!");