"""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 'old title' 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', 'Chapter

Chapter

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() def test_new_edition_uuid_keeps_obfuscated_font_readable_and_audited(tmp_path): from src.edition.fonts import decoded_fonts import uuid source,cfg,image,review,_ = fixture(tmp_path) old_id = 'urn:uuid:' + str(uuid.uuid4()) plain = b'\x00\x01\x00\x00' + bytes(range(256)) * 8 key = uuid.UUID(old_id.removeprefix('urn:uuid:')).bytes encoded = bytes(byte ^ key[i % 16] if i < 1024 else byte for i,byte in enumerate(plain)) font_source = tmp_path/'fonts.epub' with zipfile.ZipFile(source) as z, zipfile.ZipFile(font_source,'w') as target: for info in z.infolist(): data = z.read(info.filename) if info.filename == 'EPUB/package.opf': data = data.replace(b'id="old-id">9780000000001', b'id="old-id">'+old_id.encode()) data = data.replace(b'', b'') target.writestr(info,data) target.writestr('EPUB/fonts/face.ttf',encoded) target.writestr('EPUB/style.css', '@font-face {font-family: Face; src:url("fonts/face.ttf")}') target.writestr('META-INF/encryption.xml', '') config=json.loads(cfg.read_text());config['source']['identifier']=old_id;cfg.write_text(json.dumps(config)) out=tmp_path/'new-fonts.epub';record=localize(font_source,out,cfg,'target_only',image,review) assert record['identifier'] != old_id assert audit_edition(out)['issues'] == [] assert audit_pair(font_source,out)[0]['new_issues'] == [] with zipfile.ZipFile(out) as z: opf,root=read_package(out) assert list(decoded_fonts(z,opf,root).values()) == [plain] assert z.read('EPUB/fonts/face.ttf') == encoded from src.qa.headless import archive_server from urllib.request import urlopen with archive_server({'edition': out}) as url: with urlopen(url + '/edition/' + record['font_bindings'][0]['active_path']) as response: assert response.read() == plain damaged=tmp_path/'damaged-fonts.epub' active=record['font_bindings'][0]['active_path'] with zipfile.ZipFile(out) as z,zipfile.ZipFile(damaged,'w') as target: for info in z.infolist(): target.writestr(info,b'broken' if info.filename==active else z.read(info.filename)) assert any('font_binding' in issue for issue in audit_edition(damaged)['issues']) def test_metadata_comments_survive_localization(tmp_path): source,cfg,image,review,_ = fixture(tmp_path) commented=tmp_path/'commented.epub' with zipfile.ZipFile(source) as z,zipfile.ZipFile(commented,'w') as target: for info in z.infolist(): raw=z.read(info.filename) if info.filename=='EPUB/package.opf': raw=raw.replace(b'',b'') target.writestr(info,raw) out=tmp_path/'localized.epub' localize(commented,out,cfg,'target_only',image,review) assert audit_edition(out)['issues']==[] with zipfile.ZipFile(out) as z: assert b'' in z.read('EPUB/package.opf')