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_subset.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/encoding/encoding.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 "fonts/pdf_fonts/font_extraction_service.hpp"
|
||||
namespace pdfengine::parser {
|
||||
|
||||
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);
|
||||
|
||||
// 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 startPage = 0;
|
||||
@@ -639,37 +646,31 @@ std::expected<std::vector<uint8_t>, EngineError> PdfiumDocument::getFontData(con
|
||||
|
||||
FPDF_FONT font = FPDFTextObj_GetFont(obj);
|
||||
if (!font) continue;
|
||||
|
||||
size_t nameLen = FPDFFont_GetBaseFontName(font, nullptr, 0);
|
||||
if (nameLen > 0) {
|
||||
std::vector<char> nameBuf(nameLen);
|
||||
if (FPDFFont_GetBaseFontName(font, nameBuf.data(), nameLen) > 0) {
|
||||
std::string fontName(nameBuf.data());
|
||||
|
||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
||||
|
||||
if (fontDataCache_.find(fontName) == fontDataCache_.end()) {
|
||||
if (fontName == expectedFontName) {
|
||||
std::vector<uint8_t> pdfiumBytes;
|
||||
size_t buflen = 0;
|
||||
FPDFFont_GetFontData(font, nullptr, 0, &buflen);
|
||||
if (buflen > 0) {
|
||||
std::vector<uint8_t> buffer(buflen);
|
||||
pdfiumBytes.resize(buflen);
|
||||
size_t actual_len = 0;
|
||||
if (FPDFFont_GetFontData(font, buffer.data(), buflen, &actual_len)) {
|
||||
fontDataCache_[fontName] = buffer;
|
||||
} else {
|
||||
fontDataCache_[fontName] = std::vector<uint8_t>();
|
||||
}
|
||||
} else {
|
||||
fontDataCache_[fontName] = std::vector<uint8_t>();
|
||||
FPDFFont_GetFontData(font, pdfiumBytes.data(), buflen, &actual_len);
|
||||
}
|
||||
}
|
||||
|
||||
if (fontName == expectedFontName) {
|
||||
const auto& cachedBuf = fontDataCache_[fontName];
|
||||
if (!cachedBuf.empty()) {
|
||||
FPDF_ClosePage(page);
|
||||
fontDataScannedPages_ = i;
|
||||
return cachedBuf;
|
||||
|
||||
FPDF_ClosePage(page);
|
||||
fontDataScannedPages_ = i;
|
||||
|
||||
auto prog = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
|
||||
internalFontId, expectedFontName, memoryBuffer_, pdfiumBytes);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(fontsMutex_);
|
||||
if (fontDataCache_.find(expectedFontName) == fontDataCache_.end()) {
|
||||
fontDataCache_[expectedFontName] = std::vector<uint8_t>();
|
||||
}
|
||||
// If we searched all pages and didn't find the expected font name in FPDF objects,
|
||||
// let the FontExtractionService try QPDF fallback on the whole document as a last resort.
|
||||
auto finalProg = fonts::pdf_fonts::FontExtractionService::getInstance().getFontProgram(
|
||||
internalFontId, expectedFontName, memoryBuffer_, {});
|
||||
|
||||
if (finalProg) {
|
||||
return (*finalProg)->bytes;
|
||||
}
|
||||
|
||||
return std::unexpected(EngineError::FileNotFound);
|
||||
#else
|
||||
(void)internalFontId;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace pdfengine::qpdf_layer {
|
||||
|
||||
@@ -46,6 +47,7 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
|
||||
::QPDF qpdf;
|
||||
qpdf.processMemoryFile("memory", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
||||
|
||||
std::optional<QPDFObjectHandle> bestObj;
|
||||
for (QPDFObjectHandle obj : qpdf.getAllObjects()) {
|
||||
if (!obj.isDictionary()) continue;
|
||||
if (!obj.hasKey("/Type") || !obj.getKey("/Type").isName() ||
|
||||
@@ -53,43 +55,60 @@ QpdfFontExtractor::extractMapping(const std::vector<uint8_t>& pdf, const std::st
|
||||
continue;
|
||||
if (!baseFontMatches(obj, baseFontName)) continue;
|
||||
|
||||
EmbeddedFontMapping m;
|
||||
m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
|
||||
? obj.getKey("/Subtype").getName() : "";
|
||||
|
||||
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();
|
||||
bestObj = obj;
|
||||
if (obj.hasKey("/ToUnicode")) {
|
||||
break; // Found the parent Type0 font!
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
m.identityCidToGid = true;
|
||||
if (!bestObj) return std::unexpected(QpdfError::Unknown);
|
||||
QPDFObjectHandle obj = *bestObj;
|
||||
|
||||
spdlog::info("[DEBUG] Found best matching font dictionary for {}", baseFontName);
|
||||
|
||||
EmbeddedFontMapping m;
|
||||
m.subtype = obj.hasKey("/Subtype") && obj.getKey("/Subtype").isName()
|
||||
? obj.getKey("/Subtype").getName() : "";
|
||||
|
||||
if (obj.hasKey("/ToUnicode") && obj.getKey("/ToUnicode").isStream()) {
|
||||
std::string s = decodeStream(obj.getKey("/ToUnicode"));
|
||||
if (s.empty()) {
|
||||
spdlog::info("[DEBUG] ToUnicode stream is empty after decodeStream!");
|
||||
} else {
|
||||
bool parseOk = fonts::pdf_fonts::ToUnicodeParser::parse(s, m.codeToUnicode);
|
||||
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 {
|
||||
m.identityCidToGid = true;
|
||||
m.identityCidToGid = true;
|
||||
}
|
||||
} else {
|
||||
m.identityCidToGid = true;
|
||||
}
|
||||
|
||||
m.ok = m.hasToUnicode;
|
||||
return m;
|
||||
} else {
|
||||
m.identityCidToGid = true;
|
||||
}
|
||||
|
||||
m.ok = m.hasToUnicode;
|
||||
return m;
|
||||
return std::unexpected(QpdfError::Unknown);
|
||||
} catch (const QPDFExc& e) {
|
||||
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);
|
||||
#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 "qpdf_extractor.hpp"
|
||||
#include <optional>
|
||||
#include "fonts/pdf_fonts/font_extraction_types.hpp"
|
||||
|
||||
namespace pdfengine::qpdf_layer {
|
||||
|
||||
@@ -23,6 +25,9 @@ class QpdfFontExtractor {
|
||||
public:
|
||||
std::expected<EmbeddedFontMapping, QpdfError>
|
||||
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
|
||||
content_serializer_test.cpp
|
||||
qpdf_writer_test.cpp
|
||||
font_extraction_test.cpp
|
||||
)
|
||||
|
||||
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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -30,7 +30,10 @@ export function loadPdfFont(
|
||||
if (existing) return existing;
|
||||
|
||||
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;
|
||||
const hint = (cssFamilyHint || '').replace(/^[A-Z]{6}\+/, '').trim();
|
||||
const family = hint
|
||||
|
||||
@@ -74,7 +74,7 @@ function median(xs: number[]): number {
|
||||
function paraEffSize(lines: any[]): number {
|
||||
let nominal = 0;
|
||||
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;
|
||||
}
|
||||
return nominal || 12;
|
||||
@@ -105,7 +105,7 @@ function computeLayout(para: any): ParagraphLayout {
|
||||
}
|
||||
const adv = perRun[ri];
|
||||
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 });
|
||||
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;
|
||||
|
||||
// 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';
|
||||
const fallbackFamily = /times|serif/i.test(extractedFamily) ? 'Times New Roman, serif'
|
||||
: /courier|mono/i.test(extractedFamily) ? 'Courier New, monospace' : 'Arial, sans-serif';
|
||||
let extractedFamily = (domFontName || '').replace(/^[A-Z]{6}\+/, '').trim() || 'sans-serif';
|
||||
// FIX: DO NOT split on hyphens here. If the font was downloaded via @font-face (e.g. "Arial-BoldMT"),
|
||||
// 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(
|
||||
extractedFamily !== 'sans-serif' ? `'${extractedFamily}', ${fallbackFamily}` : fallbackFamily,
|
||||
);
|
||||
@@ -1011,7 +1028,7 @@ export const ParagraphEditor: React.FC<ParagraphEditorProps> = ({
|
||||
for (const ln of lines) {
|
||||
for (const r of (ln.runs ?? [])) {
|
||||
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 ?? [])) {
|
||||
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);
|
||||
|
||||
@@ -233,13 +233,29 @@ interface TextEditLayerProps {
|
||||
|
||||
function fallbackFamily(fontName: string): string {
|
||||
const n = (fontName || '').toLowerCase();
|
||||
|
||||
let baseFallback = '"Arimo", Arial, "Helvetica Neue", Helvetica, sans-serif';
|
||||
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[] {
|
||||
@@ -259,7 +275,7 @@ function flattenRuns(model: any): EditableRun[] {
|
||||
text: r.text,
|
||||
x: r.x, y: r.y, w: r.w, h: r.h,
|
||||
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,
|
||||
internalFontId: r.internal_font_id ?? '',
|
||||
fontName: r.font_name ?? '',
|
||||
@@ -281,7 +297,8 @@ function median(xs: number[]): 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 {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 173 KiB |
Reference in New Issue
Block a user