update some minor fixes
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Font Extraction API Validation Script
|
||||
======================================
|
||||
Tests font extraction APIs against real PDFs from corpus/fonts/.
|
||||
Validates: upload, document fonts, page fonts, text extraction, metadata accuracy.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import httpx
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
CORPUS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "corpus", "fonts"))
|
||||
|
||||
# PDFs to test (in priority order)
|
||||
TARGET_PDFS = [
|
||||
"utf-8.pdf",
|
||||
"vertical_text.pdf",
|
||||
"subset_font.pdf",
|
||||
]
|
||||
|
||||
REQUIRED_FONT_FIELDS = [
|
||||
"fontName", "type", "isEmbedded", "isSubset", "isVertical",
|
||||
"encoding", "cmapName", "cidSystemInfo", "subsetTag",
|
||||
"sourceType", "substitutedFrom", "substitutedTo",
|
||||
"normalizedFamily", "internalFontId", "flags",
|
||||
"ascent", "descent", "capHeight", "hasToUnicode",
|
||||
]
|
||||
|
||||
|
||||
def separator(title: str):
|
||||
print(f"\n{'='*80}")
|
||||
print(f" {title}")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
|
||||
def sub_separator(title: str):
|
||||
print(f"\n--- {title} ---\n")
|
||||
|
||||
|
||||
def validate_font_fields(font: dict, pdf_name: str, font_index: int) -> list:
|
||||
"""Validate that all required fields are present and non-null."""
|
||||
issues = []
|
||||
for field in REQUIRED_FONT_FIELDS:
|
||||
if field not in font:
|
||||
issues.append(f" [MISSING] Font #{font_index} ({font.get('fontName', '?')}): field '{field}' is missing")
|
||||
return issues
|
||||
|
||||
|
||||
def check_duplicates(fonts: list) -> list:
|
||||
"""Check for duplicate font entries."""
|
||||
seen = set()
|
||||
dupes = []
|
||||
for f in fonts:
|
||||
key = f.get("fontName", "") + "|" + f.get("type", "") + "|" + f.get("internalFontId", "")
|
||||
if key in seen:
|
||||
dupes.append(f.get("fontName", "?"))
|
||||
seen.add(key)
|
||||
return dupes
|
||||
|
||||
|
||||
def check_empty_names(fonts: list) -> list:
|
||||
"""Check for empty font names."""
|
||||
return [i for i, f in enumerate(fonts) if not f.get("fontName", "").strip()]
|
||||
|
||||
|
||||
def validate_pdf(client: httpx.Client, pdf_filename: str) -> dict:
|
||||
"""Run all validation steps for a single PDF."""
|
||||
result = {
|
||||
"filename": pdf_filename,
|
||||
"doc_id": None,
|
||||
"upload_status": None,
|
||||
"upload_response": None,
|
||||
"doc_fonts": None,
|
||||
"doc_font_count": 0,
|
||||
"page_fonts": None,
|
||||
"page_font_count": 0,
|
||||
"text_result": None,
|
||||
"glyph_count": 0,
|
||||
"font_sizes": set(),
|
||||
"issues": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
pdf_path = os.path.join(CORPUS_DIR, pdf_filename)
|
||||
if not os.path.exists(pdf_path):
|
||||
result["errors"].append(f"PDF file not found: {pdf_path}")
|
||||
return result
|
||||
|
||||
# ========== STEP 1: Upload ==========
|
||||
sub_separator(f"Step 1: Upload {pdf_filename}")
|
||||
try:
|
||||
with open(pdf_path, "rb") as f:
|
||||
resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (os.path.basename(pdf_path), f, "application/pdf")},
|
||||
)
|
||||
result["upload_status"] = resp.status_code
|
||||
result["upload_response"] = resp.json()
|
||||
print(f" Status: {resp.status_code}")
|
||||
print(f" Response: {json.dumps(resp.json(), indent=2)}")
|
||||
|
||||
if resp.status_code != 201:
|
||||
result["errors"].append(f"Upload failed with status {resp.status_code}: {resp.text}")
|
||||
return result
|
||||
|
||||
result["doc_id"] = resp.json()["id"]
|
||||
print(f" Document ID: {result['doc_id']}")
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Upload exception: {e}")
|
||||
return result
|
||||
|
||||
doc_id = result["doc_id"]
|
||||
|
||||
# ========== STEP 2: Document Font Extraction ==========
|
||||
sub_separator("Step 2: Document Font Extraction")
|
||||
try:
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
print(f" Status: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
result["errors"].append(f"Document fonts failed: {resp.status_code} - {resp.text}")
|
||||
else:
|
||||
fonts = resp.json()
|
||||
result["doc_fonts"] = fonts
|
||||
result["doc_font_count"] = len(fonts)
|
||||
print(f" Font count: {len(fonts)}")
|
||||
|
||||
# Print each font in full
|
||||
for i, f in enumerate(fonts):
|
||||
print(f"\n Font #{i}:")
|
||||
print(f" {json.dumps(f, indent=4)}")
|
||||
|
||||
# Validate font count > 0
|
||||
if len(fonts) == 0:
|
||||
result["issues"].append("Document fonts: count is 0")
|
||||
|
||||
# Validate no empty names
|
||||
empty = check_empty_names(fonts)
|
||||
if empty:
|
||||
result["issues"].append(f"Document fonts: empty font names at indices {empty}")
|
||||
|
||||
# Validate no duplicates
|
||||
dupes = check_duplicates(fonts)
|
||||
if dupes:
|
||||
result["issues"].append(f"Document fonts: duplicate fonts: {dupes}")
|
||||
|
||||
# Validate all fields present
|
||||
for i, f in enumerate(fonts):
|
||||
field_issues = validate_font_fields(f, pdf_filename, i)
|
||||
result["issues"].extend(field_issues)
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Document fonts exception: {e}")
|
||||
|
||||
# ========== STEP 3: Page Font Extraction ==========
|
||||
sub_separator("Step 3: Page Font Extraction (page 0)")
|
||||
try:
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/fonts")
|
||||
print(f" Status: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
result["errors"].append(f"Page fonts failed: {resp.status_code} - {resp.text}")
|
||||
else:
|
||||
page_fonts = resp.json()
|
||||
result["page_fonts"] = page_fonts
|
||||
result["page_font_count"] = len(page_fonts)
|
||||
print(f" Page font count: {len(page_fonts)}")
|
||||
|
||||
for i, f in enumerate(page_fonts):
|
||||
print(f"\n Page Font #{i}:")
|
||||
print(f" {json.dumps(f, indent=4)}")
|
||||
|
||||
if len(page_fonts) == 0:
|
||||
result["issues"].append("Page fonts: count is 0")
|
||||
|
||||
# Compare with document fonts
|
||||
if result["doc_fonts"] is not None:
|
||||
doc_font_names = {f["fontName"] for f in result["doc_fonts"]}
|
||||
page_font_names = {f["fontName"] for f in page_fonts}
|
||||
|
||||
print(f"\n Document font names: {sorted(doc_font_names)}")
|
||||
print(f" Page font names: {sorted(page_font_names)}")
|
||||
|
||||
# Page fonts should be a subset of document fonts
|
||||
extra_in_page = page_font_names - doc_font_names
|
||||
if extra_in_page:
|
||||
result["issues"].append(f"Page fonts not in document fonts: {extra_in_page}")
|
||||
print(f" [ISSUE] Page has fonts not in document-level: {extra_in_page}")
|
||||
else:
|
||||
print(f" [OK] Page fonts are a subset of document fonts")
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Page fonts exception: {e}")
|
||||
|
||||
# ========== STEP 4: Text Extraction ==========
|
||||
sub_separator("Step 4: Text Extraction (page 0)")
|
||||
try:
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/text")
|
||||
print(f" Status: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
result["errors"].append(f"Text extraction failed: {resp.status_code} - {resp.text}")
|
||||
else:
|
||||
text_data = resp.json()
|
||||
result["text_result"] = text_data
|
||||
text_content = text_data.get("text", "")
|
||||
glyphs = text_data.get("glyphs", [])
|
||||
result["glyph_count"] = len(glyphs)
|
||||
|
||||
print(f" Extracted text: {repr(text_content[:300])}")
|
||||
print(f" Glyph count: {len(glyphs)}")
|
||||
|
||||
if not text_content.strip():
|
||||
result["issues"].append("Text extraction: empty text")
|
||||
|
||||
if len(glyphs) == 0:
|
||||
result["issues"].append("Text extraction: no glyphs returned")
|
||||
else:
|
||||
# Show first 5 glyphs as samples
|
||||
print(f"\n First 5 glyphs (sample):")
|
||||
for i, g in enumerate(glyphs[:5]):
|
||||
print(f" Glyph #{i}: {json.dumps(g, indent=6)}")
|
||||
|
||||
# Check glyph structure
|
||||
for i, g in enumerate(glyphs):
|
||||
font_size = g.get("fontSize", 0)
|
||||
if font_size > 0:
|
||||
result["font_sizes"].add(font_size)
|
||||
elif font_size == 0 and i < 5:
|
||||
result["issues"].append(f"Glyph #{i}: fontSize is 0")
|
||||
|
||||
# Validate coordinate fields exist
|
||||
for coord in ["x", "y", "right", "bottom"]:
|
||||
if coord not in g and i < 3:
|
||||
result["issues"].append(f"Glyph #{i}: missing coordinate '{coord}'")
|
||||
|
||||
print(f"\n Font sizes detected: {sorted(result['font_sizes'])}")
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Text extraction exception: {e}")
|
||||
|
||||
# ========== STEP 5: Content-Specific Validation ==========
|
||||
sub_separator("Step 5: Content-Specific Validation")
|
||||
|
||||
if "vertical" in pdf_filename.lower() and result["doc_fonts"]:
|
||||
vertical_fonts = [f for f in result["doc_fonts"] if f.get("isVertical")]
|
||||
print(f" Vertical text PDF - fonts with isVertical=true: {len(vertical_fonts)}")
|
||||
if len(vertical_fonts) == 0:
|
||||
result["issues"].append("vertical_text.pdf: No fonts have isVertical=true")
|
||||
print(f" [ISSUE] No vertical fonts detected!")
|
||||
else:
|
||||
for vf in vertical_fonts:
|
||||
print(f" - {vf['fontName']} (isVertical=true)")
|
||||
print(f" [OK] Vertical fonts detected correctly")
|
||||
|
||||
if "subset" in pdf_filename.lower() and result["doc_fonts"]:
|
||||
subset_fonts = [f for f in result["doc_fonts"] if f.get("isSubset")]
|
||||
print(f" Subset font PDF - fonts with isSubset=true: {len(subset_fonts)}")
|
||||
if len(subset_fonts) == 0:
|
||||
result["issues"].append("subset_font.pdf: No fonts have isSubset=true")
|
||||
print(f" [ISSUE] No subset fonts detected!")
|
||||
else:
|
||||
for sf in subset_fonts:
|
||||
print(f" - {sf['fontName']} (isSubset=true, subsetTag='{sf.get('subsetTag', '')}')")
|
||||
print(f" [OK] Subset fonts detected correctly")
|
||||
|
||||
if "utf" in pdf_filename.lower() and result["text_result"]:
|
||||
text = result["text_result"].get("text", "")
|
||||
print(f" UTF-8 PDF - extracted text: {repr(text[:300])}")
|
||||
# Check for non-ASCII characters
|
||||
non_ascii = [c for c in text if ord(c) > 127]
|
||||
if non_ascii:
|
||||
print(f" Non-ASCII characters found: {len(non_ascii)} chars")
|
||||
print(f" Sample non-ASCII: {repr(''.join(non_ascii[:30]))}")
|
||||
print(f" [OK] UTF-8 text extracts with non-ASCII content")
|
||||
else:
|
||||
print(f" [INFO] No non-ASCII characters detected - content may be ASCII-only")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print(" FONT EXTRACTION API VALIDATION")
|
||||
print(f" Server: {BASE_URL}")
|
||||
print(f" Corpus: {CORPUS_DIR}")
|
||||
print(f" Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 80)
|
||||
|
||||
client = httpx.Client(base_url=BASE_URL, timeout=30.0)
|
||||
|
||||
# Verify server is up
|
||||
try:
|
||||
r = client.get("/")
|
||||
print(f"\n Server status: OK ({r.status_code})")
|
||||
except Exception as e:
|
||||
print(f"\n [FATAL] Cannot connect to server: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify corpus directory
|
||||
if not os.path.isdir(CORPUS_DIR):
|
||||
print(f"\n [FATAL] Corpus directory not found: {CORPUS_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
available = [f for f in TARGET_PDFS if os.path.exists(os.path.join(CORPUS_DIR, f))]
|
||||
print(f" PDFs to validate: {available}")
|
||||
|
||||
results = []
|
||||
for pdf in available:
|
||||
separator(f"VALIDATING: {pdf}")
|
||||
result = validate_pdf(client, pdf)
|
||||
results.append(result)
|
||||
|
||||
# ========== FINAL REPORT ==========
|
||||
separator("FINAL REPORT")
|
||||
|
||||
for r in results:
|
||||
print(f"\n{'~'*60}")
|
||||
print(f" PDF: {r['filename']}")
|
||||
print(f" Document ID: {r['doc_id']}")
|
||||
print(f" Upload Status: {r['upload_status']}")
|
||||
print(f" Document Font Count: {r['doc_font_count']}")
|
||||
print(f" Page Font Count: {r['page_font_count']}")
|
||||
print(f" Glyph Count: {r['glyph_count']}")
|
||||
print(f" Font Sizes: {sorted(r['font_sizes']) if r['font_sizes'] else 'N/A'}")
|
||||
print(f" Issues: {len(r['issues'])}")
|
||||
for issue in r['issues']:
|
||||
print(f" >> {issue}")
|
||||
print(f" Errors: {len(r['errors'])}")
|
||||
for err in r['errors']:
|
||||
print(f" XX {err}")
|
||||
|
||||
# Summary Answers
|
||||
separator("SUMMARY ANSWERS")
|
||||
|
||||
total_issues = sum(len(r["issues"]) for r in results)
|
||||
total_errors = sum(len(r["errors"]) for r in results)
|
||||
|
||||
all_fonts_extracted = all(r["doc_font_count"] > 0 for r in results if not r["errors"])
|
||||
print(f" 1. Are fonts being extracted correctly?")
|
||||
print(f" {'YES' if all_fonts_extracted else 'NO'} - {sum(r['doc_font_count'] for r in results)} total fonts across {len(results)} PDFs")
|
||||
|
||||
page_doc_consistent = all(
|
||||
not any("not in document" in i for i in r["issues"])
|
||||
for r in results
|
||||
)
|
||||
print(f"\n 2. Are page fonts and document fonts consistent?")
|
||||
print(f" {'YES' if page_doc_consistent else 'NO'}")
|
||||
|
||||
font_sizes_ok = all(r["glyph_count"] > 0 for r in results if not r["errors"])
|
||||
print(f"\n 3. Are font sizes being extracted correctly?")
|
||||
print(f" {'YES' if font_sizes_ok else 'NO'}")
|
||||
|
||||
# Check vertical/subset
|
||||
vertical_ok = True
|
||||
subset_ok = True
|
||||
for r in results:
|
||||
if "vertical" in r["filename"] and any("isVertical" in i for i in r["issues"]):
|
||||
vertical_ok = False
|
||||
if "subset" in r["filename"] and any("isSubset" in i for i in r["issues"]):
|
||||
subset_ok = False
|
||||
|
||||
print(f"\n 4. Are vertical/subset fonts detected correctly?")
|
||||
print(f" Vertical: {'YES' if vertical_ok else 'NO'}")
|
||||
print(f" Subset: {'YES' if subset_ok else 'NO'}")
|
||||
|
||||
print(f"\n 5. Are there any metadata inaccuracies?")
|
||||
if total_issues == 0 and total_errors == 0:
|
||||
print(f" NO - All {len(results)} PDFs passed validation cleanly")
|
||||
else:
|
||||
print(f" YES - {total_issues} issues and {total_errors} errors found")
|
||||
for r in results:
|
||||
for issue in r["issues"]:
|
||||
print(f" - [{r['filename']}] {issue}")
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print(f" VALIDATION COMPLETE: {total_issues} issues, {total_errors} errors")
|
||||
print(f"{'='*80}")
|
||||
|
||||
client.close()
|
||||
return 0 if total_errors == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import sys
|
||||
@@ -15,8 +16,12 @@ except ImportError:
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# Regex: six uppercase ASCII letters followed by '+'
|
||||
SUBSET_PREFIX_RE = re.compile(r'^[A-Z]{6}\+')
|
||||
|
||||
|
||||
def get_doc_id(filename: str) -> str:
|
||||
# First ensure the document is loaded
|
||||
"""Upload a PDF from corpus/fonts and return its document ID."""
|
||||
filepath = os.path.abspath(f"../corpus/fonts/{filename}")
|
||||
with open(filepath, "rb") as f:
|
||||
resp = client.post(
|
||||
@@ -26,8 +31,13 @@ def get_doc_id(filename: str) -> str:
|
||||
assert resp.status_code == 201, f"Failed to load {filename}: {resp.json()}"
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 1. Vertical font regression
|
||||
# =========================================================================
|
||||
|
||||
def test_is_vertical_regression():
|
||||
# 1. Verify Identity-V fonts are detected correctly
|
||||
"""Identity-V fonts must be flagged isVertical; horizontal fonts must not."""
|
||||
doc_id = get_doc_id("vertical_text.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
assert resp.status_code == 200
|
||||
@@ -36,8 +46,7 @@ def test_is_vertical_regression():
|
||||
font = fonts[0]
|
||||
assert font["isVertical"] is True
|
||||
assert font["encoding"] == "Identity-V"
|
||||
|
||||
# 2. Verify horizontal fonts are not falsely detected as vertical
|
||||
|
||||
doc_id_h = get_doc_id("utf-8.pdf")
|
||||
resp_h = client.get(f"/documents/{doc_id_h}/fonts")
|
||||
fonts_h = resp_h.json()
|
||||
@@ -45,20 +54,123 @@ def test_is_vertical_regression():
|
||||
for f in fonts_h:
|
||||
assert f["isVertical"] is False
|
||||
|
||||
def test_internal_font_id_regression():
|
||||
# Verify subset fonts do not duplicate subset prefixes
|
||||
|
||||
# =========================================================================
|
||||
# 2. Internal Font ID: no duplicate subset prefix (core regression)
|
||||
# =========================================================================
|
||||
|
||||
def test_internal_font_id_no_duplicate_prefix():
|
||||
"""
|
||||
Regression: subset fonts must NOT produce "ABCDEF_ABCDEF+Arial".
|
||||
The internalFontId for a subset font should be the fontName itself
|
||||
(e.g. "ABCDEF+Arial"), which already encodes the subset tag.
|
||||
"""
|
||||
doc_id = get_doc_id("subset_font.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
assert resp.status_code == 200
|
||||
fonts = resp.json()
|
||||
assert len(fonts) > 0
|
||||
|
||||
for font in fonts:
|
||||
fid = font["internalFontId"]
|
||||
tag = font.get("subsetTag", "")
|
||||
|
||||
if font.get("isSubset") and tag:
|
||||
# Must NOT start with "TAG_TAG"
|
||||
assert not fid.startswith(tag + "_" + tag), (
|
||||
f"Duplicate subset prefix detected: internalFontId='{fid}'"
|
||||
)
|
||||
# Must equal fontName directly (e.g. "ABCDEF+Arial")
|
||||
assert fid == font["fontName"], (
|
||||
f"Expected internalFontId==fontName for subset font, "
|
||||
f"got '{fid}' vs '{font['fontName']}'"
|
||||
)
|
||||
|
||||
|
||||
def test_internal_font_id_subset_format():
|
||||
"""
|
||||
For any subset font the internalFontId must match the pattern
|
||||
ABCDEF+BaseName — exactly the fontName reported by PDFium.
|
||||
"""
|
||||
doc_id = get_doc_id("text_font.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
assert resp.status_code == 200
|
||||
fonts = resp.json()
|
||||
assert len(fonts) > 0
|
||||
|
||||
for font in fonts:
|
||||
if font.get("isSubset"):
|
||||
assert font["subsetTag"] in font["fontName"]
|
||||
assert not font["internalFontId"].startswith(font["subsetTag"] + "_" + font["subsetTag"])
|
||||
# internalFontId == fontName (e.g. "ABCDEF+Arial")
|
||||
assert font["internalFontId"] == font["fontName"]
|
||||
# The ID must contain exactly one '+' from the subset tag
|
||||
assert font["internalFontId"].count("+") == 1
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 3. Non-subset font ID format
|
||||
# =========================================================================
|
||||
|
||||
def test_internal_font_id_non_subset_format():
|
||||
"""
|
||||
Non-subset fonts must have internalFontId = fontName_type_flags.
|
||||
Examples: "Helvetica_Type1_32", "Times-Roman_Type1_32".
|
||||
"""
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
assert resp.status_code == 200
|
||||
fonts = resp.json()
|
||||
assert len(fonts) > 0
|
||||
|
||||
for font in fonts:
|
||||
if not font.get("isSubset"):
|
||||
expected = f"{font['fontName']}_{font['type']}_{font['flags']}"
|
||||
assert font["internalFontId"] == expected, (
|
||||
f"Non-subset internalFontId mismatch: "
|
||||
f"got '{font['internalFontId']}', expected '{expected}'"
|
||||
)
|
||||
# Must NOT contain a '+' (no subset prefix)
|
||||
assert "+" not in font["internalFontId"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 4. ID stability across document-level and page-level APIs
|
||||
# =========================================================================
|
||||
|
||||
def test_font_id_stable_across_apis():
|
||||
"""
|
||||
The internalFontId for the same font must be identical whether queried
|
||||
from the document-level /fonts endpoint or the page-level /pages/0/fonts.
|
||||
"""
|
||||
for pdf in ("subset_font.pdf", "utf-8.pdf", "vertical_text.pdf"):
|
||||
filepath = os.path.abspath(f"../corpus/fonts/{pdf}")
|
||||
if not os.path.exists(filepath):
|
||||
continue
|
||||
|
||||
doc_id = get_doc_id(pdf)
|
||||
|
||||
doc_resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
page_resp = client.get(f"/documents/{doc_id}/pages/0/fonts")
|
||||
assert doc_resp.status_code == 200
|
||||
assert page_resp.status_code == 200
|
||||
|
||||
doc_ids = {f["fontName"]: f["internalFontId"] for f in doc_resp.json()}
|
||||
page_ids = {f["fontName"]: f["internalFontId"] for f in page_resp.json()}
|
||||
|
||||
for name in page_ids:
|
||||
assert name in doc_ids, f"Page font '{name}' not in doc fonts for {pdf}"
|
||||
assert page_ids[name] == doc_ids[name], (
|
||||
f"ID mismatch for '{name}' in {pdf}: "
|
||||
f"doc='{doc_ids[name]}' vs page='{page_ids[name]}'"
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 5. CID collection regression
|
||||
# =========================================================================
|
||||
|
||||
def test_cid_collection_regression():
|
||||
# Verify Adobe collections
|
||||
"""Adobe CID collections must use the 'Adobe-' prefix."""
|
||||
doc_id = get_doc_id("vertical_text.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
fonts = resp.json()
|
||||
@@ -67,6 +179,29 @@ def test_cid_collection_regression():
|
||||
if font.get("cidSystemInfo") and font.get("cidSystemInfo") != "None":
|
||||
assert "Adobe-" in font["cidSystemInfo"]
|
||||
|
||||
|
||||
def test_cns1_regression():
|
||||
"""Verify Adobe-CNS1 (Traditional Chinese) CID fonts and text extraction."""
|
||||
doc_id = get_doc_id("cns1_test.pdf")
|
||||
|
||||
# 1. Verify font extraction
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
assert resp.status_code == 200
|
||||
fonts = resp.json()
|
||||
assert len(fonts) > 0
|
||||
cns1_fonts = [f for f in fonts if f.get("cidSystemInfo") == "Adobe-CNS1"]
|
||||
assert len(cns1_fonts) > 0, "No Adobe-CNS1 fonts detected"
|
||||
|
||||
# 2. Verify text extraction
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/text")
|
||||
assert resp.status_code == 200
|
||||
text = resp.json()["text"]
|
||||
assert "\u4e00\u4e2d\u4ed7" in text, "Failed to extract Traditional Chinese text"
|
||||
|
||||
# =========================================================================
|
||||
# 6. UTF-8 corpus regression
|
||||
# =========================================================================
|
||||
|
||||
def test_utf8_corpus_regression():
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/text")
|
||||
@@ -75,9 +210,35 @@ def test_utf8_corpus_regression():
|
||||
assert len(data["glyphs"]) > 0
|
||||
assert len(data["text"]) > 0
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 7. Font size regression
|
||||
# =========================================================================
|
||||
|
||||
def test_font_size_regression():
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/text")
|
||||
data = resp.json()
|
||||
sizes = set(g["fontSize"] for g in data["glyphs"])
|
||||
assert len(sizes) >= 2 # utf-8.pdf should have multiple font sizes
|
||||
assert len(sizes) >= 2 # utf-8.pdf should have multiple font sizes
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 8. ID uniqueness
|
||||
# =========================================================================
|
||||
|
||||
def test_font_ids_unique_within_document():
|
||||
"""Each font within a document must have a distinct internalFontId."""
|
||||
for pdf in ("subset_font.pdf", "utf-8.pdf", "vertical_text.pdf"):
|
||||
filepath = os.path.abspath(f"../corpus/fonts/{pdf}")
|
||||
if not os.path.exists(filepath):
|
||||
continue
|
||||
|
||||
doc_id = get_doc_id(pdf)
|
||||
resp = client.get(f"/documents/{doc_id}/fonts")
|
||||
fonts = resp.json()
|
||||
|
||||
ids = [f["internalFontId"] for f in fonts]
|
||||
assert len(ids) == len(set(ids)), (
|
||||
f"Duplicate internalFontId values in {pdf}: {ids}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import pytest
|
||||
import os
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def get_doc_id(filename: str) -> str:
|
||||
"""Upload a PDF from corpus/fonts and return its document ID."""
|
||||
filepath = os.path.abspath(f"gateway/../corpus/fonts/{filename}")
|
||||
if not os.path.exists(filepath):
|
||||
filepath = os.path.abspath(f"../corpus/fonts/{filename}")
|
||||
with open(filepath, "rb") as f:
|
||||
resp = client.post(
|
||||
"/documents",
|
||||
files={"file": (filename, f, "application/pdf")}
|
||||
)
|
||||
assert resp.status_code == 201, f"Failed to load {filename}: {resp.json()}"
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
def test_glyph_width_proportionality():
|
||||
"""Verify that wide characters return larger glyph widths than narrow characters."""
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
|
||||
# 1. Fetch fonts for the page
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/fonts")
|
||||
assert resp.status_code == 200
|
||||
fonts = resp.json()
|
||||
assert len(fonts) > 0, "No fonts extracted from utf-8.pdf"
|
||||
|
||||
# Find an embedded or substituted font name
|
||||
font_name = fonts[0]["fontName"]
|
||||
|
||||
# 2. Get width of wide character 'W' (charcode 87)
|
||||
w_resp = client.get(
|
||||
f"/documents/{doc_id}/pages/0/fonts/glyph-width",
|
||||
params={"font_name": font_name, "charcode": 87, "font_size": 12.0}
|
||||
)
|
||||
assert w_resp.status_code == 200
|
||||
w_width = w_resp.json()["width"]
|
||||
assert w_width > 0.0
|
||||
|
||||
# 3. Get width of narrow character 'i' (charcode 105)
|
||||
i_resp = client.get(
|
||||
f"/documents/{doc_id}/pages/0/fonts/glyph-width",
|
||||
params={"font_name": font_name, "charcode": 105, "font_size": 12.0}
|
||||
)
|
||||
assert i_resp.status_code == 200
|
||||
i_width = i_resp.json()["width"]
|
||||
assert i_width > 0.0
|
||||
|
||||
# 'W' must be strictly wider than 'i' in proportional typefaces
|
||||
assert w_width > i_width, f"Expected width('W') > width('i'), got {w_width} vs {i_width}"
|
||||
|
||||
|
||||
def test_glyph_width_font_size_scaling():
|
||||
"""Verify that glyph width scales proportionally with font size."""
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
resp = client.get(f"/documents/{doc_id}/pages/0/fonts")
|
||||
fonts = resp.json()
|
||||
font_name = fonts[0]["fontName"]
|
||||
|
||||
# Width at 12pt
|
||||
resp_12 = client.get(
|
||||
f"/documents/{doc_id}/pages/0/fonts/glyph-width",
|
||||
params={"font_name": font_name, "charcode": 65, "font_size": 12.0}
|
||||
)
|
||||
assert resp_12.status_code == 200
|
||||
width_12 = resp_12.json()["width"]
|
||||
|
||||
# Width at 24pt
|
||||
resp_24 = client.get(
|
||||
f"/documents/{doc_id}/pages/0/fonts/glyph-width",
|
||||
params={"font_name": font_name, "charcode": 65, "font_size": 24.0}
|
||||
)
|
||||
assert resp_24.status_code == 200
|
||||
width_24 = resp_24.json()["width"]
|
||||
|
||||
# Scaling must be linear: width(24pt) = 2.0 * width(12pt)
|
||||
assert pytest.approx(width_24) == 2.0 * width_12
|
||||
|
||||
|
||||
def test_glyph_width_invalid_font():
|
||||
"""Verify that querying an invalid font returns a bad request error."""
|
||||
doc_id = get_doc_id("utf-8.pdf")
|
||||
resp = client.get(
|
||||
f"/documents/{doc_id}/pages/0/fonts/glyph-width",
|
||||
params={"font_name": "NonExistentFontName123", "charcode": 65, "font_size": 12.0}
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "detail" in resp.json()
|
||||
@@ -137,6 +137,10 @@ def test_extract_page_text(client: TestClient):
|
||||
first_glyph = glyphs[0]
|
||||
for key in ["text", "x", "y", "w", "h", "fontSize"]:
|
||||
assert key in first_glyph
|
||||
|
||||
for g in glyphs:
|
||||
assert g["fontSize"] != 1.0, f"Fake fontSize 1.0 detected for glyph: {g}"
|
||||
assert g["text"] not in ["\r", "\n"], f"Control character detected in glyph bounds: {g}"
|
||||
|
||||
def test_apply_edits_and_incremental_save(client: TestClient):
|
||||
with open(HELLO_WORLD_PDF, "rb") as f:
|
||||
@@ -266,7 +270,7 @@ def test_font_size_and_diagnostics_advanced(client: TestClient):
|
||||
assert font["sourceType"] == "Embedded"
|
||||
assert len(font["subsetTag"]) == 6
|
||||
assert font["subsetTag"].isupper()
|
||||
assert font["internalFontId"] == f"{font['subsetTag']}_{font['fontName']}"
|
||||
assert font["internalFontId"] == font["fontName"]
|
||||
else:
|
||||
assert len(font["subsetTag"]) == 0
|
||||
assert font["internalFontId"] == f"{font['fontName']}_{font['type']}_{font['flags']}"
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
================================================================================
|
||||
FONT EXTRACTION API VALIDATION
|
||||
Server: http://localhost:8000
|
||||
Corpus: C:\Users\Maskan\Desktop\pdf_editor\pdf\corpus\fonts
|
||||
Timestamp: 2026-06-02 12:36:47
|
||||
================================================================================
|
||||
|
||||
Server status: OK (200)
|
||||
PDFs to validate: ['utf-8.pdf', 'vertical_text.pdf', 'subset_font.pdf']
|
||||
|
||||
================================================================================
|
||||
VALIDATING: utf-8.pdf
|
||||
================================================================================
|
||||
|
||||
|
||||
--- Step 1: Upload utf-8.pdf ---
|
||||
|
||||
Status: 201
|
||||
Response: {
|
||||
"id": "6764e35e-3594-4b50-a378-3ea122a54d9d",
|
||||
"filename": "utf-8.pdf",
|
||||
"sizeBytes": 1275,
|
||||
"totalPages": 1,
|
||||
"uploadedAt": "2026-06-02T07:06:50.429369Z",
|
||||
"status": "ready"
|
||||
}
|
||||
Document ID: 6764e35e-3594-4b50-a378-3ea122a54d9d
|
||||
|
||||
--- Step 2: Document Font Extraction ---
|
||||
|
||||
Status: 200
|
||||
Font count: 2
|
||||
|
||||
Font #0:
|
||||
{
|
||||
"fontName": "Helvetica",
|
||||
"type": "Type1",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "SystemFallback",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Helvetica",
|
||||
"internalFontId": "Helvetica_Type1_32",
|
||||
"flags": 32,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
Font #1:
|
||||
{
|
||||
"fontName": "Times-Roman",
|
||||
"type": "Type1",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "SystemFallback",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Times",
|
||||
"internalFontId": "Times-Roman_Type1_32",
|
||||
"flags": 32,
|
||||
"ascent": 891.0,
|
||||
"descent": -216.0,
|
||||
"capHeight": 662.0
|
||||
}
|
||||
|
||||
--- Step 3: Page Font Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Page font count: 2
|
||||
|
||||
Page Font #0:
|
||||
{
|
||||
"fontName": "Helvetica",
|
||||
"type": "Type1",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "SystemFallback",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Helvetica",
|
||||
"internalFontId": "Helvetica_Type1_32",
|
||||
"flags": 32,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
Page Font #1:
|
||||
{
|
||||
"fontName": "Times-Roman",
|
||||
"type": "Type1",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "SystemFallback",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Times",
|
||||
"internalFontId": "Times-Roman_Type1_32",
|
||||
"flags": 32,
|
||||
"ascent": 891.0,
|
||||
"descent": -216.0,
|
||||
"capHeight": 662.0
|
||||
}
|
||||
|
||||
Document font names: ['Helvetica', 'Times-Roman']
|
||||
Page font names: ['Helvetica', 'Times-Roman']
|
||||
[OK] Page fonts are a subset of document fonts
|
||||
|
||||
--- Step 4: Text Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Extracted text: 'Hello World - UTF-8 Test Document\r\nStandard Latin Text for Encoding Verification\r\nFont Size Detection Sample: Small Text 12pt\r\nLARGE TEXT FOR SIZE 18PT DETECTION\r\nMore 18pt content: ABCDEFGHabcdefgh 0123456789\r\nBack to 12pt: The quick brown fox jumps over the lazy dog\r\nSpecial chars: copyright secti'
|
||||
Glyph count: 312
|
||||
|
||||
First 5 glyphs (sample):
|
||||
Glyph #0: {
|
||||
"text": "H",
|
||||
"x": 72.95999908447266,
|
||||
"y": 720.0,
|
||||
"w": 6.7440032958984375,
|
||||
"h": 8.59197998046875,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #1: {
|
||||
"text": "e",
|
||||
"x": 81.10800170898438,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.736000061035156,
|
||||
"h": 6.49200439453125,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #2: {
|
||||
"text": "l",
|
||||
"x": 88.10400390625,
|
||||
"y": 720.0,
|
||||
"w": 1.055999755859375,
|
||||
"h": 8.59197998046875,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #3: {
|
||||
"text": "l",
|
||||
"x": 90.76799774169922,
|
||||
"y": 720.0,
|
||||
"w": 1.055999755859375,
|
||||
"h": 8.59197998046875,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #4: {
|
||||
"text": "o",
|
||||
"x": 93.05999755859375,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.832000732421875,
|
||||
"h": 6.49200439453125,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
|
||||
Font sizes detected: [1.0, 12.0, 18.0]
|
||||
|
||||
--- Step 5: Content-Specific Validation ---
|
||||
|
||||
UTF-8 PDF - extracted text: 'Hello World - UTF-8 Test Document\r\nStandard Latin Text for Encoding Verification\r\nFont Size Detection Sample: Small Text 12pt\r\nLARGE TEXT FOR SIZE 18PT DETECTION\r\nMore 18pt content: ABCDEFGHabcdefgh 0123456789\r\nBack to 12pt: The quick brown fox jumps over the lazy dog\r\nSpecial chars: copyright secti'
|
||||
[INFO] No non-ASCII characters detected - content may be ASCII-only
|
||||
|
||||
================================================================================
|
||||
VALIDATING: vertical_text.pdf
|
||||
================================================================================
|
||||
|
||||
|
||||
--- Step 1: Upload vertical_text.pdf ---
|
||||
|
||||
Status: 201
|
||||
Response: {
|
||||
"id": "d28fd4e4-a059-4a9b-bf15-b6cf25caf280",
|
||||
"filename": "vertical_text.pdf",
|
||||
"sizeBytes": 3518,
|
||||
"totalPages": 1,
|
||||
"uploadedAt": "2026-06-02T07:06:50.465835Z",
|
||||
"status": "ready"
|
||||
}
|
||||
Document ID: d28fd4e4-a059-4a9b-bf15-b6cf25caf280
|
||||
|
||||
--- Step 2: Document Font Extraction ---
|
||||
|
||||
Status: 200
|
||||
Font count: 1
|
||||
|
||||
Font #0:
|
||||
{
|
||||
"fontName": "Test",
|
||||
"type": "TrueType",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": true,
|
||||
"encoding": "Identity-V",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "Identity-V",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "Substituted",
|
||||
"substitutedFrom": "Test",
|
||||
"substitutedTo": "Arial",
|
||||
"normalizedFamily": "Test",
|
||||
"internalFontId": "Test_TrueType_524320",
|
||||
"flags": 524320,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
--- Step 3: Page Font Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Page font count: 1
|
||||
|
||||
Page Font #0:
|
||||
{
|
||||
"fontName": "Test",
|
||||
"type": "TrueType",
|
||||
"isEmbedded": false,
|
||||
"isSubset": false,
|
||||
"isVertical": true,
|
||||
"encoding": "Identity-V",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "Identity-V",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "",
|
||||
"sourceType": "Substituted",
|
||||
"substitutedFrom": "Test",
|
||||
"substitutedTo": "Arial",
|
||||
"normalizedFamily": "Test",
|
||||
"internalFontId": "Test_TrueType_524320",
|
||||
"flags": 524320,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
Document font names: ['Test']
|
||||
Page font names: ['Test']
|
||||
[OK] Page fonts are a subset of document fonts
|
||||
|
||||
--- Step 4: Text Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Extracted text: 'Hello World!\r\nHello'
|
||||
Glyph count: 19
|
||||
|
||||
First 5 glyphs (sample):
|
||||
Glyph #0: {
|
||||
"text": "H",
|
||||
"x": 6.832000255584717,
|
||||
"y": 180.1840057373047,
|
||||
"w": 6.552000522613525,
|
||||
"h": 8.699996948242188,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #1: {
|
||||
"text": "e",
|
||||
"x": 7.324000358581543,
|
||||
"y": 171.39999389648438,
|
||||
"w": 5.495999336242676,
|
||||
"h": 6.756011962890625,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #2: {
|
||||
"text": "l",
|
||||
"x": 9.687999725341797,
|
||||
"y": 160.49200439453125,
|
||||
"w": 1.055999755859375,
|
||||
"h": 9.251998901367188,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #3: {
|
||||
"text": "l",
|
||||
"x": 9.687999725341797,
|
||||
"y": 149.4759979248047,
|
||||
"w": 1.055999755859375,
|
||||
"h": 9.251998901367188,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #4: {
|
||||
"text": "o",
|
||||
"x": 7.324000358581543,
|
||||
"y": 140.69200134277344,
|
||||
"w": 5.951999664306641,
|
||||
"h": 6.7559967041015625,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
|
||||
Font sizes detected: [1.0, 12.0]
|
||||
|
||||
--- Step 5: Content-Specific Validation ---
|
||||
|
||||
Vertical text PDF - fonts with isVertical=true: 1
|
||||
- Test (isVertical=true)
|
||||
[OK] Vertical fonts detected correctly
|
||||
|
||||
================================================================================
|
||||
VALIDATING: subset_font.pdf
|
||||
================================================================================
|
||||
|
||||
|
||||
--- Step 1: Upload subset_font.pdf ---
|
||||
|
||||
Status: 201
|
||||
Response: {
|
||||
"id": "cf3f2218-6ee7-4e34-973d-6e1b61c13cf8",
|
||||
"filename": "subset_font.pdf",
|
||||
"sizeBytes": 646,
|
||||
"totalPages": 1,
|
||||
"uploadedAt": "2026-06-02T07:06:50.497336Z",
|
||||
"status": "ready"
|
||||
}
|
||||
Document ID: cf3f2218-6ee7-4e34-973d-6e1b61c13cf8
|
||||
|
||||
--- Step 2: Document Font Extraction ---
|
||||
|
||||
Status: 200
|
||||
Font count: 1
|
||||
|
||||
Font #0:
|
||||
{
|
||||
"fontName": "ABCDEF+Arial",
|
||||
"type": "TrueType",
|
||||
"isEmbedded": true,
|
||||
"isSubset": true,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "ABCDEF",
|
||||
"sourceType": "Embedded",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Arial",
|
||||
"internalFontId": "ABCDEF_ABCDEF+Arial",
|
||||
"flags": 0,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
--- Step 3: Page Font Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Page font count: 1
|
||||
|
||||
Page Font #0:
|
||||
{
|
||||
"fontName": "ABCDEF+Arial",
|
||||
"type": "TrueType",
|
||||
"isEmbedded": true,
|
||||
"isSubset": true,
|
||||
"isVertical": false,
|
||||
"encoding": "WinAnsiEncoding",
|
||||
"hasToUnicode": true,
|
||||
"cmapName": "None",
|
||||
"cidSystemInfo": "None",
|
||||
"subsetTag": "ABCDEF",
|
||||
"sourceType": "Embedded",
|
||||
"substitutedFrom": "",
|
||||
"substitutedTo": "",
|
||||
"normalizedFamily": "Arial",
|
||||
"internalFontId": "ABCDEF_ABCDEF+Arial",
|
||||
"flags": 0,
|
||||
"ascent": 905.0,
|
||||
"descent": -211.0,
|
||||
"capHeight": 728.0
|
||||
}
|
||||
|
||||
Document font names: ['ABCDEF+Arial']
|
||||
Page font names: ['ABCDEF+Arial']
|
||||
[OK] Page fonts are a subset of document fonts
|
||||
|
||||
--- Step 4: Text Extraction (page 0) ---
|
||||
|
||||
Status: 200
|
||||
Extracted text: 'Subset Text'
|
||||
Glyph count: 11
|
||||
|
||||
First 5 glyphs (sample):
|
||||
Glyph #0: {
|
||||
"text": "S",
|
||||
"x": 72.54000091552734,
|
||||
"y": 719.8679809570312,
|
||||
"w": 6.839996337890625,
|
||||
"h": 8.8680419921875,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #1: {
|
||||
"text": "u",
|
||||
"x": 80.77200317382812,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.0399932861328125,
|
||||
"h": 6.36004638671875,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #2: {
|
||||
"text": "b",
|
||||
"x": 87.45600128173828,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.400001525878906,
|
||||
"h": 8.7239990234375,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #3: {
|
||||
"text": "s",
|
||||
"x": 93.72000122070312,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.159996032714844,
|
||||
"h": 6.49200439453125,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
Glyph #4: {
|
||||
"text": "e",
|
||||
"x": 99.79199981689453,
|
||||
"y": 719.8679809570312,
|
||||
"w": 5.736000061035156,
|
||||
"h": 6.49200439453125,
|
||||
"fontSize": 12.0
|
||||
}
|
||||
|
||||
Font sizes detected: [12.0]
|
||||
|
||||
--- Step 5: Content-Specific Validation ---
|
||||
|
||||
Subset font PDF - fonts with isSubset=true: 1
|
||||
- ABCDEF+Arial (isSubset=true, subsetTag='ABCDEF')
|
||||
[OK] Subset fonts detected correctly
|
||||
|
||||
================================================================================
|
||||
FINAL REPORT
|
||||
================================================================================
|
||||
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
PDF: utf-8.pdf
|
||||
Document ID: 6764e35e-3594-4b50-a378-3ea122a54d9d
|
||||
Upload Status: 201
|
||||
Document Font Count: 2
|
||||
Page Font Count: 2
|
||||
Glyph Count: 312
|
||||
Font Sizes: [1.0, 12.0, 18.0]
|
||||
Issues: 6
|
||||
>> Glyph #0: missing coordinate 'right'
|
||||
>> Glyph #0: missing coordinate 'bottom'
|
||||
>> Glyph #1: missing coordinate 'right'
|
||||
>> Glyph #1: missing coordinate 'bottom'
|
||||
>> Glyph #2: missing coordinate 'right'
|
||||
>> Glyph #2: missing coordinate 'bottom'
|
||||
Errors: 0
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
PDF: vertical_text.pdf
|
||||
Document ID: d28fd4e4-a059-4a9b-bf15-b6cf25caf280
|
||||
Upload Status: 201
|
||||
Document Font Count: 1
|
||||
Page Font Count: 1
|
||||
Glyph Count: 19
|
||||
Font Sizes: [1.0, 12.0]
|
||||
Issues: 6
|
||||
>> Glyph #0: missing coordinate 'right'
|
||||
>> Glyph #0: missing coordinate 'bottom'
|
||||
>> Glyph #1: missing coordinate 'right'
|
||||
>> Glyph #1: missing coordinate 'bottom'
|
||||
>> Glyph #2: missing coordinate 'right'
|
||||
>> Glyph #2: missing coordinate 'bottom'
|
||||
Errors: 0
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
PDF: subset_font.pdf
|
||||
Document ID: cf3f2218-6ee7-4e34-973d-6e1b61c13cf8
|
||||
Upload Status: 201
|
||||
Document Font Count: 1
|
||||
Page Font Count: 1
|
||||
Glyph Count: 11
|
||||
Font Sizes: [12.0]
|
||||
Issues: 6
|
||||
>> Glyph #0: missing coordinate 'right'
|
||||
>> Glyph #0: missing coordinate 'bottom'
|
||||
>> Glyph #1: missing coordinate 'right'
|
||||
>> Glyph #1: missing coordinate 'bottom'
|
||||
>> Glyph #2: missing coordinate 'right'
|
||||
>> Glyph #2: missing coordinate 'bottom'
|
||||
Errors: 0
|
||||
|
||||
================================================================================
|
||||
SUMMARY ANSWERS
|
||||
================================================================================
|
||||
|
||||
1. Are fonts being extracted correctly?
|
||||
YES - 4 total fonts across 3 PDFs
|
||||
|
||||
2. Are page fonts and document fonts consistent?
|
||||
YES
|
||||
|
||||
3. Are font sizes being extracted correctly?
|
||||
YES
|
||||
|
||||
4. Are vertical/subset fonts detected correctly?
|
||||
Vertical: YES
|
||||
Subset: YES
|
||||
|
||||
5. Are there any metadata inaccuracies?
|
||||
YES - 18 issues and 0 errors found
|
||||
- [utf-8.pdf] Glyph #0: missing coordinate 'right'
|
||||
- [utf-8.pdf] Glyph #0: missing coordinate 'bottom'
|
||||
- [utf-8.pdf] Glyph #1: missing coordinate 'right'
|
||||
- [utf-8.pdf] Glyph #1: missing coordinate 'bottom'
|
||||
- [utf-8.pdf] Glyph #2: missing coordinate 'right'
|
||||
- [utf-8.pdf] Glyph #2: missing coordinate 'bottom'
|
||||
- [vertical_text.pdf] Glyph #0: missing coordinate 'right'
|
||||
- [vertical_text.pdf] Glyph #0: missing coordinate 'bottom'
|
||||
- [vertical_text.pdf] Glyph #1: missing coordinate 'right'
|
||||
- [vertical_text.pdf] Glyph #1: missing coordinate 'bottom'
|
||||
- [vertical_text.pdf] Glyph #2: missing coordinate 'right'
|
||||
- [vertical_text.pdf] Glyph #2: missing coordinate 'bottom'
|
||||
- [subset_font.pdf] Glyph #0: missing coordinate 'right'
|
||||
- [subset_font.pdf] Glyph #0: missing coordinate 'bottom'
|
||||
- [subset_font.pdf] Glyph #1: missing coordinate 'right'
|
||||
- [subset_font.pdf] Glyph #1: missing coordinate 'bottom'
|
||||
- [subset_font.pdf] Glyph #2: missing coordinate 'right'
|
||||
- [subset_font.pdf] Glyph #2: missing coordinate 'bottom'
|
||||
|
||||
================================================================================
|
||||
VALIDATION COMPLETE: 18 issues, 0 errors
|
||||
================================================================================
|
||||
Reference in New Issue
Block a user