cursor and fontissue
This commit is contained in:
@@ -29,6 +29,8 @@ add_library(pdfengine OBJECT
|
|||||||
src/fonts/pdf_fonts/font_fallback.cpp
|
src/fonts/pdf_fonts/font_fallback.cpp
|
||||||
src/fonts/pdf_fonts/font_subset.cpp
|
src/fonts/pdf_fonts/font_subset.cpp
|
||||||
src/fonts/pdf_fonts/font_cmap_builder.cpp
|
src/fonts/pdf_fonts/font_cmap_builder.cpp
|
||||||
|
src/fonts/pdf_fonts/font_validator.cpp
|
||||||
|
src/fonts/pdf_fonts/font_extraction_service.cpp
|
||||||
src/fonts/pdf_fonts/embedded_font_reconstructor.cpp
|
src/fonts/pdf_fonts/embedded_font_reconstructor.cpp
|
||||||
src/fonts/pdf_fonts/encoding/encoding.cpp
|
src/fonts/pdf_fonts/encoding/encoding.cpp
|
||||||
src/fonts/pdf_fonts/encoding/tounicode_parser.cpp
|
src/fonts/pdf_fonts/encoding/tounicode_parser.cpp
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "font_extraction_service.hpp"
|
||||||
|
#include "font_validator.hpp"
|
||||||
|
#include "../../qpdf/qpdf_font_extractor.hpp"
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
namespace pdfengine::fonts::pdf_fonts {
|
||||||
|
|
||||||
|
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
|
||||||
|
FontExtractionService::getFontProgram(const std::string& internalFontId,
|
||||||
|
const std::string& baseFontName,
|
||||||
|
const std::vector<uint8_t>& documentBuffer,
|
||||||
|
const std::vector<uint8_t>& pdfiumRawBytes) {
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||||
|
if (cache_.count(internalFontId)) {
|
||||||
|
return cache_[internalFontId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Try PDFium Bytes (Standard fonts)
|
||||||
|
if (!pdfiumRawBytes.empty() && FontValidator::isValidSFNT(pdfiumRawBytes)) {
|
||||||
|
auto prog = std::make_shared<EmbeddedFontProgram>();
|
||||||
|
prog->fontName = baseFontName;
|
||||||
|
prog->bytes = pdfiumRawBytes;
|
||||||
|
prog->source = FontExtractionSource::PDFium;
|
||||||
|
|
||||||
|
cache_[internalFontId] = prog;
|
||||||
|
spdlog::info("[FontExtraction] internalId='{}' source=PDFium size={}", internalFontId, prog->bytes.size());
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try QPDF Fallback (Type0 CIDFonts)
|
||||||
|
if (!documentBuffer.empty()) {
|
||||||
|
qpdf_layer::QpdfFontExtractor qpdfExtractor;
|
||||||
|
auto qpdfProg = qpdfExtractor.extractFontProgram(documentBuffer, baseFontName);
|
||||||
|
if (qpdfProg && FontValidator::isValidSFNT(qpdfProg->bytes)) {
|
||||||
|
auto prog = std::make_shared<EmbeddedFontProgram>(*qpdfProg);
|
||||||
|
prog->source = FontExtractionSource::QPDF_Raw;
|
||||||
|
cache_[internalFontId] = prog;
|
||||||
|
spdlog::info("[FontExtraction] internalId='{}' source=QPDF_Raw size={}", internalFontId, prog->bytes.size());
|
||||||
|
return prog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 2 Reconstruction would happen here if needed, but for now we return not found
|
||||||
|
spdlog::warn("[FontExtraction] Failed to extract valid SFNT for '{}'", internalFontId);
|
||||||
|
return std::unexpected(EngineError::FileNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pdfengine::fonts::pdf_fonts
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
|
#include <expected>
|
||||||
|
|
||||||
|
#include "font_extraction_types.hpp"
|
||||||
|
#include <pdfengine/pdf_document.hpp>
|
||||||
|
|
||||||
|
namespace pdfengine::fonts::pdf_fonts {
|
||||||
|
|
||||||
|
class FontExtractionService {
|
||||||
|
public:
|
||||||
|
static FontExtractionService& getInstance() {
|
||||||
|
static FontExtractionService instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::expected<std::shared_ptr<EmbeddedFontProgram>, EngineError>
|
||||||
|
getFontProgram(const std::string& internalFontId,
|
||||||
|
const std::string& baseFontName,
|
||||||
|
const std::vector<uint8_t>& documentBuffer,
|
||||||
|
const std::vector<uint8_t>& pdfiumRawBytes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
FontExtractionService() = default;
|
||||||
|
|
||||||
|
std::mutex cacheMutex_;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<EmbeddedFontProgram>> cache_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pdfengine::fonts::pdf_fonts
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pdfengine::fonts::pdf_fonts {
|
||||||
|
|
||||||
|
enum class FontProgramType { Unknown, TrueType, CFF, OpenType, Type1 };
|
||||||
|
|
||||||
|
enum class FontExtractionSource { PDFium, QPDF_Raw, QPDF_Reconstructed, Cache };
|
||||||
|
|
||||||
|
struct EmbeddedFontProgram {
|
||||||
|
std::string fontName;
|
||||||
|
std::vector<uint8_t> bytes;
|
||||||
|
FontProgramType type = FontProgramType::Unknown;
|
||||||
|
FontExtractionSource source = FontExtractionSource::PDFium;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pdfengine::fonts::pdf_fonts
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "font_validator.hpp"
|
||||||
|
|
||||||
|
namespace pdfengine::fonts::pdf_fonts {
|
||||||
|
|
||||||
|
bool FontValidator::isValidSFNT(const std::vector<uint8_t>& bytes) {
|
||||||
|
if (bytes.size() < 12) { // Minimum SFNT header size
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check SFNT version / magic bytes
|
||||||
|
// 0x00010000 for TrueType
|
||||||
|
// 0x4F54544F ("OTTO") for OpenType CFF
|
||||||
|
// 0x74727565 ("true") for Apple TrueType
|
||||||
|
// 0x74797031 ("typ1") for Mac PostScript Type 1
|
||||||
|
const uint8_t* b = bytes.data();
|
||||||
|
uint32_t magic = (static_cast<uint32_t>(b[0]) << 24) |
|
||||||
|
(static_cast<uint32_t>(b[1]) << 16) |
|
||||||
|
(static_cast<uint32_t>(b[2]) << 8) |
|
||||||
|
static_cast<uint32_t>(b[3]);
|
||||||
|
|
||||||
|
if (magic == 0x00010000 || magic == 0x4F54544F || magic == 0x74727565 || magic == 0x74797031) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pdfengine::fonts::pdf_fonts
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace pdfengine::fonts::pdf_fonts {
|
||||||
|
|
||||||
|
class FontValidator {
|
||||||
|
public:
|
||||||
|
static bool isValidSFNT(const std::vector<uint8_t>& bytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pdfengine::fonts::pdf_fonts
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "parser/pdfium_internal.hpp"
|
#include "parser/pdfium_internal.hpp"
|
||||||
|
#include "fonts/pdf_fonts/font_extraction_service.hpp"
|
||||||
namespace pdfengine::parser {
|
namespace pdfengine::parser {
|
||||||
|
|
||||||
void PdfiumDocument::registerAuxFont(const std::string& internalFontId, const std::vector<uint8_t>& sfnt) {
|
void PdfiumDocument::registerAuxFont(const std::string& internalFontId, const std::vector<uint8_t>& sfnt) {
|
||||||
@@ -607,6 +607,13 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
|
|||||||
|
|
||||||
std::string expectedFontName = baseNameFromInternalFontId(internalFontId);
|
std::string expectedFontName = baseNameFromInternalFontId(internalFontId);
|
||||||
|
|
||||||
|
// 1. Try cache via service
|
||||||
|
auto cachedProg = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
|
||||||
|
internalFontId, expectedFontName, memoryBuffer_, {});
|
||||||
|
if (cachedProg) {
|
||||||
|
return (*cachedProg)->bytes;
|
||||||
|
}
|
||||||
|
|
||||||
int numPages = FPDF_GetPageCount(doc_);
|
int numPages = FPDF_GetPageCount(doc_);
|
||||||
|
|
||||||
int startPage = 0;
|
int startPage = 0;
|
||||||
@@ -639,37 +646,31 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
|
|||||||
|
|
||||||
FPDF_FONT font = FPDFTextObj_GetFont(obj);
|
FPDF_FONT font = FPDFTextObj_GetFont(obj);
|
||||||
if (!font) continue;
|
if (!font) continue;
|
||||||
|
|
||||||
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
|
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
|
||||||
if (nameLen > 0) {
|
if (nameLen > 0) {
|
||||||
std::vector<char> nameBuf(nameLen);
|
std::vector<char> nameBuf(nameLen);
|
||||||
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
|
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
|
||||||
std::string fontName(nameBuf.data());
|
std::string fontName(nameBuf.data());
|
||||||
|
if (fontName == expectedFontName) {
|
||||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
std::vector<uint8_t> pdfiumBytes;
|
||||||
|
|
||||||
if (fontDataCache_.find(fontName) == fontDataCache_.end()) {
|
|
||||||
size_t buflen = 0;
|
size_t buflen = 0;
|
||||||
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
|
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
|
||||||
if (buflen > 0) {
|
if (buflen > 0) {
|
||||||
std::vector<uint8_t> buffer(buflen);
|
pdfiumBytes.resize(buflen);
|
||||||
size_t actual_len = 0;
|
size_t actual_len = 0;
|
||||||
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) {
|
FPDFFont_GetFontData(font, pdfiumBytes.data(), buflen, &actual_len);
|
||||||
fontDataCache_[fontName] = buffer;
|
|
||||||
} else {
|
|
||||||
fontDataCache_[fontName] = std::vector<uint8_t>();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fontDataCache_[fontName] = std::vector<uint8_t>();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
FPDF_ClosePage(page);
|
||||||
if (fontName == expectedFontName) {
|
fontDataScannedPages_ = i;
|
||||||
const auto& cachedBuf = fontDataCache_[fontName];
|
|
||||||
if (!cachedBuf.empty()) {
|
auto prog = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
|
||||||
FPDF_ClosePage(page);
|
internalFontId, expectedFontName, memoryBuffer_, pdfiumBytes);
|
||||||
fontDataScannedPages_ = i;
|
|
||||||
return cachedBuf;
|
if (prog) {
|
||||||
|
return (*prog)->bytes;
|
||||||
|
} else {
|
||||||
|
return std::unexpected(prog.error());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,12 +682,15 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
|
|||||||
fontDataScannedPages_ = i + 1;
|
fontDataScannedPages_ = i + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// If we searched all pages and didn't find the expected font name in FPDF objects,
|
||||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
// let the FontExtractionService try QPDF fallback on the whole document as a last resort.
|
||||||
if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) {
|
auto finalProg = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
|
||||||
fontDataCache_[expectedFontName] = std::vector<uint8_t>();
|
internalFontId, expectedFontName, memoryBuffer_, {});
|
||||||
}
|
|
||||||
|
if (finalProg) {
|
||||||
|
return (*finalProg)->bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::unexpected(EngineError::FileNotFound);
|
return std::unexpected(EngineError::FileNotFound);
|
||||||
#else
|
#else
|
||||||
(void)internalFontId;
|
(void)internalFontId;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
namespace pdfengine::qpdf_layer {
|
namespace pdfengine::qpdf_layer {
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
|
|||||||
::QPDF qpdf;
|
::QPDF qpdf;
|
||||||
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
||||||
|
|
||||||
|
std::optional<QPDFObjectHandle> bestObj;
|
||||||
for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
|
for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
|
||||||
if (!obj.isDictionary()) continue;
|
if (!obj.isDictionary()) continue;
|
||||||
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
|
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
|
||||||
@@ -53,43 +55,60 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
|
|||||||
continue;
|
continue;
|
||||||
if (!baseFontMatches(obj, baseFontName)) continue;
|
if (!baseFontMatches(obj, baseFontName)) continue;
|
||||||
|
|
||||||
EmbeddedFontMapping m;
|
bestObj = obj;
|
||||||
m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
|
if (obj.hasKey("/ToUnicode")) {
|
||||||
? obj.getKey("/Subtype").getName() : "";
|
break; // Found the parent Type0 font!
|
||||||
|
|
||||||
if (obj.hasKey("/ToUnicode") && obj.getKey("/ToUnicode").isStream()) {
|
|
||||||
std::string s = decodeStream(obj.getKey("/ToUnicode"));
|
|
||||||
if (!s.empty() && fonts::pdf_fonts::ToUnicodeParser::parse(s, m.codeToUnicode))
|
|
||||||
m.hasToUnicode = !m.codeToUnicode.empty();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m.subtype == "/Type0" && obj.hasKey("/DescendantFonts")) {
|
if (!bestObj) return std::unexpected(QpdfError::Unknown);
|
||||||
QPDFObjectHandle df = obj.getKey("/DescendantFonts");
|
QPDFObjectHandle obj = *bestObj;
|
||||||
QPDFObjectHandle cid = df.isArray() && df.getArrayNItems() > 0 ? df.getArrayItem(0)
|
|
||||||
: QPDFObjectHandle();
|
spdlog::info("[DEBUG] Found best matching font dictionary for {}", baseFontName);
|
||||||
if (cid.isDictionary() && cid.hasKey("/CIDToGIDMap")) {
|
|
||||||
QPDFObjectHandle c2g = cid.getKey("/CIDToGIDMap");
|
EmbeddedFontMapping m;
|
||||||
if (c2g.isStream()) {
|
m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
|
||||||
std::string s = decodeStream(c2g);
|
? obj.getKey("/Subtype").getName() : "";
|
||||||
m.identityCidToGid = false;
|
|
||||||
for (size_t i = 0; i + 1 < s.size(); i += 2) {
|
if (obj.hasKey("/ToUnicode") && obj.getKey("/ToUnicode").isStream()) {
|
||||||
uint16_t gid = static_cast<uint16_t>((static_cast<uint8_t>(s[i]) << 8) |
|
std::string s = decodeStream(obj.getKey("/ToUnicode"));
|
||||||
static_cast<uint8_t>(s[i + 1]));
|
if (s.empty()) {
|
||||||
if (gid != 0) m.codeToGid[static_cast<uint32_t>(i / 2)] = gid;
|
spdlog::info("[DEBUG] ToUnicode stream is empty after decodeStream!");
|
||||||
}
|
} else {
|
||||||
} else {
|
bool parseOk = fonts::pdf_fonts::ToUnicodeParser::parse(s, m.codeToUnicode);
|
||||||
m.identityCidToGid = true;
|
if (!parseOk) {
|
||||||
|
spdlog::info("[DEBUG] ToUnicodeParser::parse returned false!");
|
||||||
|
}
|
||||||
|
m.hasToUnicode = !m.codeToUnicode.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m.subtype == "/Type0" && obj.hasKey("/DescendantFonts")) {
|
||||||
|
QPDFObjectHandle df = obj.getKey("/DescendantFonts");
|
||||||
|
QPDFObjectHandle cid = df.isArray() && df.getArrayNItems() > 0 ? df.getArrayItem(0)
|
||||||
|
: QPDFObjectHandle();
|
||||||
|
if (cid.isDictionary() && cid.hasKey("/CIDToGIDMap")) {
|
||||||
|
QPDFObjectHandle c2g = cid.getKey("/CIDToGIDMap");
|
||||||
|
if (c2g.isStream()) {
|
||||||
|
std::string s = decodeStream(c2g);
|
||||||
|
m.identityCidToGid = false;
|
||||||
|
for (size_t i = 0; i + 1 < s.size(); i += 2) {
|
||||||
|
uint16_t gid = static_cast<uint16_t>((static_cast<uint8_t>(s[i]) << 8) |
|
||||||
|
static_cast<uint8_t>(s[i + 1]));
|
||||||
|
if (gid != 0) m.codeToGid[static_cast<uint32_t>(i / 2)] = gid;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.identityCidToGid = true;
|
m.identityCidToGid = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.identityCidToGid = true;
|
m.identityCidToGid = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
m.ok = m.hasToUnicode;
|
m.identityCidToGid = true;
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.ok = m.hasToUnicode;
|
||||||
|
return m;
|
||||||
return std::unexpected(QpdfError::Unknown);
|
return std::unexpected(QpdfError::Unknown);
|
||||||
} catch (const QPDFExc& e) {
|
} catch (const QPDFExc& e) {
|
||||||
fprintf(stderr, "QpdfFontExtractor QPDFExc: %s\n", e.what());
|
fprintf(stderr, "QpdfFontExtractor QPDFExc: %s\n", e.what());
|
||||||
@@ -103,5 +122,72 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
|
|||||||
return std::unexpected(QpdfError::NotSupported);
|
return std::unexpected(QpdfError::NotSupported);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
std::optional<fonts::pdf_fonts::EmbeddedFontProgram>
|
||||||
|
QpdfFontExtractor::extractFontProgram(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const {
|
||||||
|
#ifdef PDFENGINE_WITH_QPDF
|
||||||
|
try {
|
||||||
|
::QPDF qpdf;
|
||||||
|
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
||||||
|
|
||||||
|
for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
|
||||||
|
if (!obj.isDictionary()) continue;
|
||||||
|
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
|
||||||
|
obj.getKey("/Type").getName() != "/Font")
|
||||||
|
continue;
|
||||||
|
if (!baseFontMatches(obj, baseFontName)) continue;
|
||||||
|
|
||||||
|
std::string subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
|
||||||
|
? obj.getKey("/Subtype").getName() : "";
|
||||||
|
|
||||||
|
QPDFObjectHandle fontDesc;
|
||||||
|
|
||||||
|
if (subtype == "/Type0" && obj.hasKey("/DescendantFonts")) {
|
||||||
|
QPDFObjectHandle df = obj.getKey("/DescendantFonts");
|
||||||
|
if (df.isArray() && df.getArrayNItems() > 0) {
|
||||||
|
QPDFObjectHandle cid = df.getArrayItem(0);
|
||||||
|
if (cid.isDictionary() && cid.hasKey("/FontDescriptor")) {
|
||||||
|
fontDesc = cid.getKey("/FontDescriptor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj.hasKey("/FontDescriptor")) {
|
||||||
|
fontDesc = obj.getKey("/FontDescriptor");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fontDesc.isDictionary()) continue;
|
||||||
|
|
||||||
|
QPDFObjectHandle streamObj;
|
||||||
|
fonts::pdf_fonts::FontProgramType ftype = fonts::pdf_fonts::FontProgramType::Unknown;
|
||||||
|
|
||||||
|
if (fontDesc.hasKey("/FontFile2") && fontDesc.getKey("/FontFile2").isStream()) {
|
||||||
|
streamObj = fontDesc.getKey("/FontFile2");
|
||||||
|
ftype = fonts::pdf_fonts::FontProgramType::TrueType;
|
||||||
|
} else if (fontDesc.hasKey("/FontFile3") && fontDesc.getKey("/FontFile3").isStream()) {
|
||||||
|
streamObj = fontDesc.getKey("/FontFile3");
|
||||||
|
ftype = fonts::pdf_fonts::FontProgramType::CFF;
|
||||||
|
} else if (fontDesc.hasKey("/FontFile") && fontDesc.getKey("/FontFile").isStream()) {
|
||||||
|
streamObj = fontDesc.getKey("/FontFile");
|
||||||
|
ftype = fonts::pdf_fonts::FontProgramType::Type1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamObj.isStream()) {
|
||||||
|
std::string s = decodeStream(streamObj);
|
||||||
|
if (!s.empty()) {
|
||||||
|
fonts::pdf_fonts::EmbeddedFontProgram fp;
|
||||||
|
fp.fontName = baseFontName;
|
||||||
|
fp.bytes = std::vector<uint8_t>(s.begin(), s.end());
|
||||||
|
fp.type = ftype;
|
||||||
|
fp.source = fonts::pdf_fonts::FontExtractionSource::QPDF_Raw;
|
||||||
|
return fp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
fprintf(stderr, "QpdfFontExtractor extractFontProgram exception: %s\n", e.what());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)pdf; (void)baseFontName;
|
||||||
|
#endif
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "qpdf_extractor.hpp"
|
#include "qpdf_extractor.hpp"
|
||||||
|
#include <optional>
|
||||||
|
#include "fonts/pdf_fonts/font_extraction_types.hpp"
|
||||||
|
|
||||||
namespace pdfengine::qpdf_layer {
|
namespace pdfengine::qpdf_layer {
|
||||||
|
|
||||||
@@ -23,6 +25,9 @@ class QpdfFontExtractor {
|
|||||||
public:
|
public:
|
||||||
std::expected<EmbeddedFontMapping, QpdfError>
|
std::expected<EmbeddedFontMapping, QpdfError>
|
||||||
extractMapping(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const;
|
extractMapping(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const;
|
||||||
|
|
||||||
|
std::optional<fonts::pdf_fonts::EmbeddedFontProgram>
|
||||||
|
extractFontProgram(const std::vector<uint8_t>& pdf, const std::string& baseFontName) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ add_executable(pdfengine_smoke
|
|||||||
ast_serializer_test.cpp
|
ast_serializer_test.cpp
|
||||||
content_serializer_test.cpp
|
content_serializer_test.cpp
|
||||||
qpdf_writer_test.cpp
|
qpdf_writer_test.cpp
|
||||||
|
font_extraction_test.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(PDFENGINE_WITH_QPDF)
|
if(PDFENGINE_WITH_QPDF)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include "../src/fonts/pdf_fonts/font_extraction_service.hpp"
|
||||||
|
#include "../src/fonts/pdf_fonts/font_validator.hpp"
|
||||||
|
|
||||||
|
using namespace pdfengine::fonts::pdf_fonts;
|
||||||
|
|
||||||
|
TEST(FontValidatorTest, ValidatesSFNT) {
|
||||||
|
// TrueType Magic: 0x00010000
|
||||||
|
std::vector<uint8_t> ttf = {0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
EXPECT_TRUE(FontValidator::isValidSFNT(ttf));
|
||||||
|
|
||||||
|
// OTTO Magic
|
||||||
|
std::vector<uint8_t> otto = {'O', 'T', 'T', 'O', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
EXPECT_TRUE(FontValidator::isValidSFNT(otto));
|
||||||
|
|
||||||
|
// Invalid Magic
|
||||||
|
std::vector<uint8_t> invalid = {0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
EXPECT_FALSE(FontValidator::isValidSFNT(invalid));
|
||||||
|
|
||||||
|
// Too small
|
||||||
|
std::vector<uint8_t> small = {0x00, 0x01, 0x00, 0x00};
|
||||||
|
EXPECT_FALSE(FontValidator::isValidSFNT(small));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FontExtractionServiceTest, FallbacksToQPDFIfPdfiumFails) {
|
||||||
|
auto& service = FontExtractionService::getInstance();
|
||||||
|
|
||||||
|
// We expect it to return FileNotFound because we provided empty buffers
|
||||||
|
// but this ensures the code compiles and the service instance is accessible.
|
||||||
|
auto res = service.getFontProgram("F1", "TestFont", {}, {});
|
||||||
|
EXPECT_FALSE(res.has_value());
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PDF Editor</title>
|
<title>PDF Editor</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="h-full w-full overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased">
|
<body class="h-full w-full overflow-hidden bg-[#f1f2f4] text-[#18212e] antialiased">
|
||||||
<div id="root" class="flex h-full w-full flex-col"></div>
|
<div id="root" class="flex h-full w-full flex-col"></div>
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ export function loadPdfFont(
|
|||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
|
|
||||||
const p = (async (): Promise<string | null> => {
|
const p = (async (): Promise<string | null> => {
|
||||||
const bytes = await gatewayService.getFontData(documentId, internalFontId);
|
let bytes = await gatewayService.getReconstructedFontData(documentId, internalFontId);
|
||||||
|
if (!bytes || bytes.byteLength === 0) {
|
||||||
|
bytes = await gatewayService.getFontData(documentId, internalFontId);
|
||||||
|
}
|
||||||
if (!bytes || bytes.byteLength === 0) return null;
|
if (!bytes || bytes.byteLength === 0) return null;
|
||||||
const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim();
|
const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim();
|
||||||
const family = hint
|
const family = hint
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function median(xs: number[]): number {
|
|||||||
function paraEffSize(lines: any[]): number {
|
function paraEffSize(lines: any[]): number {
|
||||||
let nominal = 0;
|
let nominal = 0;
|
||||||
for (const line of lines) for (const r of (line.runs ?? [])) {
|
for (const line of lines) for (const r of (line.runs ?? [])) {
|
||||||
const sz = Math.max(r.font_size ?? 0, r.h ?? 0);
|
const sz = r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8;
|
||||||
if (sz > nominal) nominal = sz;
|
if (sz > nominal) nominal = sz;
|
||||||
}
|
}
|
||||||
return nominal || 12;
|
return nominal || 12;
|
||||||
@@ -105,7 +105,7 @@ function computeLayout(para: any): ParagraphLayout {
|
|||||||
}
|
}
|
||||||
const adv = perRun[ri];
|
const adv = perRun[ri];
|
||||||
const safeColor = sanitizeTextColor(r.color);
|
const safeColor = sanitizeTextColor(r.color);
|
||||||
const rSize = Math.max(r.font_size ?? 0, r.h ?? 0) || effSize;
|
const rSize = (r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8) || effSize;
|
||||||
seedRuns.push({ text, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, fontName: r.font_name ?? '', advances: adv });
|
seedRuns.push({ text, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, fontName: r.font_name ?? '', advances: adv });
|
||||||
if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, advances: adv });
|
if (orig) lineFrags.push({ text: orig, fid: r.internal_font_id ?? '', size: rSize, color: safeColor, advances: adv });
|
||||||
}
|
}
|
||||||
@@ -631,9 +631,26 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
|||||||
const colWidthPx = (columnRight - columnLeft) * zoom;
|
const colWidthPx = (columnRight - columnLeft) * zoom;
|
||||||
|
|
||||||
// Fix 1: use extracted PDF font name (loaded via @font-face), not a generic Arial/Times/Courier map.
|
// Fix 1: use extracted PDF font name (loaded via @font-face), not a generic Arial/Times/Courier map.
|
||||||
const extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif';
|
let extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif';
|
||||||
const fallbackFamily = /times|serif/i.test(extractedFamily) ? 'Times New Roman, serif'
|
// FIX: DO NOT split on hyphens here. If the font was downloaded via @font-face (e.g. "Arial-BoldMT"),
|
||||||
: /courier|mono/i.test(extractedFamily) ? 'Courier New, monospace' : 'Arial, sans-serif';
|
// we must use the exact string "Arial-BoldMT" so the browser maps to the downloaded font, not the OS font.
|
||||||
|
|
||||||
|
// FIX: Chrome on Windows forcefully aliases exactly "Helvetica" to "Arial" at the OS layer.
|
||||||
|
// We bypass this hardcoded alias only if the original name is EXACTLY Helvetica or Arial.
|
||||||
|
const rawFamily = extractedFamily.toLowerCase();
|
||||||
|
if (rawFamily === 'helvetica' || rawFamily === 'arial') {
|
||||||
|
extractedFamily = `"Inter", ${extractedFamily}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallbackFamily = '"Arimo", Arial, sans-serif';
|
||||||
|
if (/times|serif/i.test(extractedFamily)) {
|
||||||
|
fallbackFamily = 'Times New Roman, serif';
|
||||||
|
} else if (/courier|mono/i.test(extractedFamily)) {
|
||||||
|
fallbackFamily = 'Courier New, monospace';
|
||||||
|
} else {
|
||||||
|
fallbackFamily = '"Inter", "Arimo", Arial, sans-serif';
|
||||||
|
}
|
||||||
|
|
||||||
const [measureFamily, setMeasureFamily] = useState(
|
const [measureFamily, setMeasureFamily] = useState(
|
||||||
extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily,
|
extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily,
|
||||||
);
|
);
|
||||||
@@ -1011,7 +1028,7 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
|||||||
for (const ln of lines) {
|
for (const ln of lines) {
|
||||||
for (const r of (ln.runs ?? [])) {
|
for (const r of (ln.runs ?? [])) {
|
||||||
pdfFontName = r.font_name || pdfFontName;
|
pdfFontName = r.font_name || pdfFontName;
|
||||||
pdfFontSize = Math.max(pdfFontSize, r.font_size ?? 0, r.h ?? 0);
|
pdfFontSize = Math.max(pdfFontSize, r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8);
|
||||||
for (const g of (r.glyphs ?? [])) {
|
for (const g of (r.glyphs ?? [])) {
|
||||||
const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h;
|
const x0 = g.bbox_x, y0 = g.bbox_y, x1 = g.bbox_x + g.bbox_w, y1 = g.bbox_y + g.bbox_h;
|
||||||
pdfMinX = Math.min(pdfMinX, x0); pdfMinY = Math.min(pdfMinY, y0);
|
pdfMinX = Math.min(pdfMinX, x0); pdfMinY = Math.min(pdfMinY, y0);
|
||||||
|
|||||||
@@ -233,13 +233,29 @@ interface TextEditLayerProps {
|
|||||||
|
|
||||||
function fallbackFamily(fontName: string): string {
|
function fallbackFamily(fontName: string): string {
|
||||||
const n = (fontName || '').toLowerCase();
|
const n = (fontName || '').toLowerCase();
|
||||||
|
|
||||||
|
let baseFallback = '"Arimo", Arial, "Helvetica Neue", Helvetica, sans-serif';
|
||||||
if (n.includes('times') || (n.includes('serif') && !n.includes('sans'))) {
|
if (n.includes('times') || (n.includes('serif') && !n.includes('sans'))) {
|
||||||
return '"Times New Roman", Times, Georgia, serif';
|
baseFallback = '"Times New Roman", Times, Georgia, serif';
|
||||||
|
} else if (n.includes('courier') || n.includes('mono')) {
|
||||||
|
baseFallback = '"Courier New", Courier, monospace';
|
||||||
|
} else {
|
||||||
|
baseFallback = '"Inter", "Arimo", Arial, "Helvetica Neue", Helvetica, sans-serif';
|
||||||
}
|
}
|
||||||
if (n.includes('courier') || n.includes('mono')) {
|
|
||||||
return '"Courier New", Courier, monospace';
|
let cleanName = (fontName || '').replace(/^[A-Z]{6}\+/, '').trim();
|
||||||
|
|
||||||
|
// FIX: Chrome on Windows forcefully aliases exactly "Helvetica" to "Arial" at the OS layer.
|
||||||
|
// By explicitly returning Inter here, we bypass this hardcoded alias so it renders the modern font instead of Arial.
|
||||||
|
const rawName = cleanName.toLowerCase();
|
||||||
|
if (rawName === 'helvetica' || rawName === 'arial') {
|
||||||
|
return `"Inter", ${baseFallback}`;
|
||||||
}
|
}
|
||||||
return 'Arial, "Helvetica Neue", Helvetica, sans-serif';
|
|
||||||
|
if (cleanName && !['arial', 'helvetica', 'times', 'courier', 'arimo', 'inter'].includes(rawName)) {
|
||||||
|
return `"${cleanName}", ${baseFallback}`;
|
||||||
|
}
|
||||||
|
return baseFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenRuns(model: any): EditableRun[] {
|
function flattenRuns(model: any): EditableRun[] {
|
||||||
@@ -259,7 +275,7 @@ function flattenRuns(model: any): EditableRun[] {
|
|||||||
text: r.text,
|
text: r.text,
|
||||||
x: r.x, y: r.y, w: r.w, h: r.h,
|
x: r.x, y: r.y, w: r.w, h: r.h,
|
||||||
baselineY,
|
baselineY,
|
||||||
fontSize: Math.max(r.font_size ?? 0, r.h ?? 0),
|
fontSize: r.font_size && r.font_size > 0 ? r.font_size : (r.h ?? 0) * 0.8,
|
||||||
objectIndices,
|
objectIndices,
|
||||||
internalFontId: r.internal_font_id ?? '',
|
internalFontId: r.internal_font_id ?? '',
|
||||||
fontName: r.font_name ?? '',
|
fontName: r.font_name ?? '',
|
||||||
@@ -281,7 +297,8 @@ function median(xs: number[]): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function displayFontSize(r: EditableRun): number {
|
function displayFontSize(r: EditableRun): number {
|
||||||
return Math.max(r.fontSize ?? 0, r.h ?? 0);
|
if (r.fontSize && r.fontSize > 0) return r.fontSize;
|
||||||
|
return (r.h ?? 0) * 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReflowPayload(model: any, run: EditableRun, newText: string): ReflowParagraphPayload | null {
|
function buildReflowPayload(model: any, run: EditableRun, newText: string): ReflowParagraphPayload | null {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 173 KiB |
Reference in New Issue
Block a user