update some minor fixes

This commit is contained in:
saqib mir
2026-06-03 10:51:33 +05:30
parent dc6e538c81
commit 0862afa1ce
14 changed files with 1405 additions and 14 deletions
+388
View File
@@ -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())
+170 -9
View File
@@ -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}"
)
+93
View File
@@ -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()
+5 -1
View File
@@ -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']}"
+554
View File
@@ -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
================================================================================