"""Reader-facing edition identity and cover binding, EPUB2/3 and missing-cover cases."""
import base64
import json
import zipfile
from pathlib import Path
import pytest
from lxml import etree
from src.edition.localize import localize, prepare, sha, read_package, dc_values, audit_edition, edition_record, localize_from_config
from src.qa.headless import audit_pair
PNG = base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=')
OLD = PNG + b'original'
def fixture(tmp_path, version='3.0', has_cover=True, svg=False):
path = tmp_path/'input.epub'
cover_meta = '' if has_cover else ''
cover_item = ' ' if has_cover else ''
cover_doc = '' if svg else '
'
opf = f'''Original WorkAn Author9780000000001enOriginal Publisher{cover_meta}mainAuthor, An{cover_item} '''
with zipfile.ZipFile(path, 'w') as z:
z.writestr('mimetype', 'application/epub+zip', compress_type=zipfile.ZIP_STORED)
z.writestr('META-INF/container.xml', '')
z.writestr('EPUB/package.opf', opf)
z.writestr('EPUB/cover.xhtml', '
Cover'+ (cover_doc if has_cover else 'Title page
') + '')
z.writestr('EPUB/chapter.xhtml', 'ChapterChapter
Keep my text and illustration.
')
z.writestr('EPUB/toc.ncx', 'Original WorkChapter')
z.writestr('EPUB/images/body.png', OLD)
if has_cover: z.writestr('EPUB/images/old.png', OLD)
cfg = tmp_path/'edition.json'
config = {'source': {'titles':['Original Work'], 'authors':['An Author'], 'identifier':'9780000000001'},
'title_zh':'权威书名', 'authors_zh':['作者译名'], 'title_status':'authoritative',
'evidence':[{'url':'https://publisher.example/book'}], 'edition_revision':'luna-r2'}
cfg.write_text(json.dumps(config)); image = tmp_path/'generated.any'; image.write_bytes(PNG)
review = tmp_path/'review.json'
def review_for(mode):
review.write_text(json.dumps({'cover_sha256':sha(PNG), 'mode':mode, 'title_zh':config['title_zh'],
'edition_revision':'luna-r2', 'visual_review':'passed', 'provider':'test fixture', 'model':None}))
review_for('target_only')
return path, cfg, image, review, review_for
@pytest.mark.parametrize('version', ['2.0', '3.0'])
@pytest.mark.parametrize('svg', [False, True])
def test_reader_cover_identity_and_body_preservation(tmp_path, version, svg):
source, cfg, image, review, update = fixture(tmp_path, version, svg=svg)
ids = []
for mode in ['target_only','bilingual']:
update(mode); out = tmp_path/(mode+'.epub')
record = localize(source, out, cfg, mode, image, review); ids.append(record['identifier'])
_, root = read_package(out)
assert dc_values(root,'title') == [record['title']]
assert dc_values(root,'creator') == ['作者译名']
assert dc_values(root,'identifier') == [record['identifier']]
assert '9780000000001' in dc_values(root,'source')
assert dc_values(root,'language') == (['zh-CN'] if mode == 'target_only' else ['zh-CN','en'])
assert not root.xpath('//*[@refines="#title" or @refines="#author"]')
assert audit_edition(out)['issues'] == []
report, _, _ = audit_pair(source, out); assert report['new_issues'] == []
with zipfile.ZipFile(out) as z, zipfile.ZipFile(source) as original:
assert z.read('EPUB/chapter.xhtml') == original.read('EPUB/chapter.xhtml')
assert z.read('EPUB/images/old.png') == OLD
assert z.read('EPUB/images/body.png') == OLD
assert z.read(record['cover_path']) == PNG
assert record['cover_path'].endswith('.png')
doc=etree.fromstring(z.read('EPUB/cover.xhtml'))
ref = doc.xpath('//*[local-name()="image"]/@*[local-name()="href"]') if svg else doc.xpath('//*[local-name()="img"]/@src')
assert ref == [Path(record['cover_path']).name]
if svg: assert doc.xpath('//*[local-name()="svg"]/@preserveAspectRatio') == ['xMidYMid meet']
assert z.infolist()[0].filename == 'mimetype' and z.infolist()[0].compress_type == zipfile.ZIP_STORED
assert ids[0] != ids[1]
def test_absent_cover_adds_front_page_and_preserves_navigation(tmp_path):
source,cfg,image,review,_ = fixture(tmp_path,has_cover=False)
job = prepare(source,cfg,'target_only',tmp_path/'job'); assert job['reference'] is None
out=tmp_path/'new.epub'; localize(source,out,cfg,'target_only',image,review)
_,root=read_package(out)
assert root.xpath('//*[local-name()="spine"]/*[1]/@idref') == ['translation-cover-page']
assert audit_pair(source,out)[0]['new_issues'] == []
def test_fail_closed_for_wrong_book_unreviewed_cover_and_no_authority(tmp_path):
source,cfg,image,review,_ = fixture(tmp_path)
review.write_text('{}')
with pytest.raises(ValueError,match='Cover review'): localize(source,tmp_path/'out.epub',cfg,'target_only',image,review)
data=json.loads(cfg.read_text()); data['evidence']=[]; cfg.write_text(json.dumps(data))
with pytest.raises(ValueError,match='evidence'): prepare(source,cfg,'target_only',tmp_path/'job')
data['title_status']='project'; data['source']['titles']=['Wrong book']; cfg.write_text(json.dumps(data))
with pytest.raises(ValueError,match='does not match'): prepare(source,cfg,'target_only',tmp_path/'job')
def test_corrupt_cover_is_detected_by_headless_audit(tmp_path):
source,cfg,image,review,_=fixture(tmp_path); out=tmp_path/'out.epub'
record=localize(source,out,cfg,'target_only',image,review)
corrupted=tmp_path/'corrupt.epub'
with zipfile.ZipFile(out) as z, zipfile.ZipFile(corrupted,'w') as target:
for info in z.infolist(): target.writestr(info,b'broken' if info.filename==record['cover_path'] else z.read(info.filename))
assert 'edition_cover_hash_mismatch' in audit_edition(corrupted)['issues']
assert any(x['kind']=='edition_cover_hash_mismatch' for x in audit_pair(source,corrupted)[0]['new_issues'])
def test_pipeline_asset_paths_are_relative_to_config(tmp_path):
source,cfg,image,review,_=fixture(tmp_path)
data=json.loads(cfg.read_text()); data['covers']={'target_only':{'image':image.name,'review':review.name}}; cfg.write_text(json.dumps(data))
output=localize_from_config(source,cfg,'target_only'); assert output.exists()
with pytest.raises(ValueError,match='Already localized'): localize(output,tmp_path/'again.epub',cfg,'target_only',image,review)
with pytest.raises(ValueError,match='overwrite'): localize(source,source,cfg,'target_only',image,review)
def test_external_svg_cover_wrapper_is_cloned_not_overwritten(tmp_path):
source,cfg,image,review,_=fixture(tmp_path)
original=tmp_path/'wrapped.epub'
svg=b''
with zipfile.ZipFile(source) as z,zipfile.ZipFile(original,'w') as target:
for info in z.infolist():
data=z.read(info.filename)
if info.filename=='EPUB/package.opf':
data=data.replace(b'',b' ')
if info.filename=='EPUB/cover.xhtml': data=data.replace(b'images/old.png',b'images/wrapper.svg')
target.writestr(info,data)
target.writestr('EPUB/images/wrapper.svg',svg)
out=tmp_path/'localized.epub'; record=localize(original,out,cfg,'target_only',image,review)
with zipfile.ZipFile(out) as z:
assert z.read('EPUB/images/wrapper.svg')==svg
assert b'translation-wrapper-' in z.read('EPUB/cover.xhtml')
assert len(record['cover_bindings'])==2
assert audit_pair(original,out)[0]['new_issues']==[]
def test_headless_can_render_a_new_cover_without_a_source_counterpart(tmp_path):
import os
if not os.environ.get('PLAYWRIGHT_BROWSERS_PATH'):
pytest.skip('Optional browser integration; set PLAYWRIGHT_BROWSERS_PATH')
pytest.importorskip('playwright.sync_api')
from src.qa.headless import book_inventory, render_pair
source,cfg,image,review,_ = fixture(tmp_path,has_cover=False)
out=tmp_path/'edition.epub'; record=localize(source,out,cfg,'target_only',image,review)
cover_page=record['cover_bindings'][0]['file']
evidence=tmp_path/'evidence';evidence.mkdir()
result=render_pair(source,out,book_inventory(source),book_inventory(out),evidence,[cover_page],1,1)
assert result['sampled_documents']==[cover_page]
assert len(result['images'])==1 and result['images'][0]['book']=='translated'
assert (evidence/result['images'][0]['image']).exists()