68 lines
5.1 KiB
Python
68 lines
5.1 KiB
Python
"""Compare cached source PDF pages and actual EPUB source anchors locally."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
|
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('epub',type=Path);parser.add_argument('--work',type=Path,required=True)
|
|
parser.add_argument('--pages',default='1,4,16');parser.add_argument('--out',type=Path,required=True)
|
|
args=parser.parse_args()
|
|
from src.qa.headless import archive_server,book_inventory,LAYOUT_JS
|
|
from src.qa.epub_audit import audit_epub
|
|
from playwright.sync_api import sync_playwright
|
|
data=json.loads((args.work/'book.json').read_text())
|
|
pages={p['number']:p for p in data['pages']};selected=[int(p) for p in args.pages.split(',')]
|
|
if any(p not in pages for p in selected):parser.error('Requested page not present in the conversion IR')
|
|
args.out.mkdir(parents=True,exist_ok=True)
|
|
inventory=book_inventory(args.epub);evidence=[];clicks=[];sections=[]
|
|
with archive_server({'book':args.epub}) as base,sync_playwright() as pw:
|
|
browser=pw.chromium.launch(headless=True)
|
|
try:
|
|
context=browser.new_context(java_script_enabled=False,service_workers='block')
|
|
context.route('**/*',lambda route:route.continue_() if route.request.url.startswith(base+'/') else route.abort())
|
|
tab=context.new_page()
|
|
for number in selected:
|
|
anchor=f'pdf-page-{number}'
|
|
document=next((name for name,soup in inventory['documents'].items() if soup.find(id=anchor)),None)
|
|
if not document:raise ValueError(f'EPUB lost source anchor {anchor}')
|
|
source_name=f'pdf-{number:04d}.jpg';shutil.copy2(pages[number]['render'],args.out/source_name)
|
|
rendered=[]
|
|
for width in [900,390]:
|
|
tab.set_viewport_size({'width':width,'height':1100})
|
|
tab.goto(base+'/book/'+quote(document)+'#'+anchor,wait_until='networkidle')
|
|
tab.evaluate('() => document.fonts.ready')
|
|
tab.evaluate('(id) => {const e=document.getElementById(id); scrollTo(0,Math.max(0,e.getBoundingClientRect().top+scrollY-140));}',anchor)
|
|
name=f'epub-{number:04d}-{width}.png';tab.screenshot(path=str(args.out/name));rendered.append(name)
|
|
evidence.append({'source_page':number,'document':document,'viewport_width':width,'image':name,'layout_findings':tab.evaluate(LAYOUT_JS)})
|
|
sections.append(f'<section><h2>PDF 原页序 {number}</h2><div class="pair"><img src="{source_name}" alt="原扫描页"/><img src="{rendered[0]}" alt="EPUB实际排版"/><img src="{rendered[1]}" alt="窄屏排版"/></div></section>')
|
|
# Real browser navigation for every generated TOC target, including fragments.
|
|
for entry in inventory['toc']:
|
|
target=entry['target'];name,_,fragment=target.partition('#')
|
|
nav=entry['file']
|
|
nav_soup=inventory['documents'].get(nav)
|
|
if nav_soup is not None:
|
|
from src.qa.headless import destination
|
|
anchors=nav_soup.find_all('a',href=True)
|
|
link_index=next(i for i,a in enumerate(anchors) if destination(nav,a['href'])==target)
|
|
tab.goto(base+'/book/'+quote(nav),wait_until='domcontentloaded')
|
|
tab.locator('a[href]').nth(link_index).click(timeout=5000)
|
|
else:
|
|
tab.goto(base+'/book/'+quote(name)+(('#'+quote(fragment)) if fragment else ''),wait_until='domcontentloaded')
|
|
reached=(tab.url.split('#')[0]==base+'/book/'+quote(name)) and (not fragment or tab.evaluate('(id) => !!document.getElementById(id)',fragment))
|
|
clicks.append({'target':target,'destination_reached':reached})
|
|
finally:browser.close()
|
|
report={'source_sha256':data['source_sha256'],'epub_sha256':hashlib.sha256(args.epub.read_bytes()).hexdigest(),'epub_audit':audit_epub(args.epub),'evidence':evidence,'toc_navigation_checks':clicks,'visual_review':'pending','ocr_accuracy_review':'pending','limits':'Chromium continuous XHTML, not all reader pagination; source anchor may be inside a continued paragraph.'}
|
|
(args.out/'report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
|
|
(args.out/'review.html').write_text('<!doctype html><meta charset="utf-8"><title>PDF → EPUB QA</title><style>body{font:16px sans-serif;margin:24px}.pair{display:grid;grid-template-columns:1fr 1fr .5fr;gap:16px}img{width:100%;height:auto;align-self:start}section{margin-bottom:40px}</style><h1>PDF → EPUB 对照</h1><p>左:原扫描;中:EPUB;右:窄屏。截图生成不等于视觉或OCR验收。</p>'+''.join(sections))
|
|
print(f'PDF source comparisons: {len(selected)}; TOC target navigation: {len(clicks)}')
|
|
return int(bool(report['epub_audit']['issues']) or any(not c['destination_reached'] for c in clicks) or any(e['layout_findings'] for e in evidence))
|
|
|
|
if __name__=='__main__':sys.exit(main())
|