"""PDF主题和资源政策回归;没有真正的提供者或用户文件。""" import asyncio import base64 from io import BytesIO import pytest from PIL import Image from pydantic import ValidationError from app.contracts import ExportAsset, ExportOptions, ExportRequest from app.export.assets import validate_assets, enrich_document, source_hash from app.export.exporters.pdf import PdfExporter from app.export.markdown import parse_document from app.export.themes import PALETTES from app.export import service def png_asset(size=(40,30), source='graph LR; A-->B'): out=BytesIO(); Image.new('RGBA',size,(0,0,0,0)).save(out,'PNG') return ExportAsset(kind='mermaid',source_hash=source_hash(source),png_base64=base64.b64encode(out.getvalue()).decode()) @pytest.mark.parametrize('theme',list(PALETTES)) def test_pdf_theme_colors_are_written_on_every_page(theme): import re, zlib palette=PALETTES[theme] doc=parse_document(('## Section\n\nText body\n\n> Quoted text\n\n```python\nprint(1)\n```\n\n')*30) result=PdfExporter().render(doc,ExportOptions(theme_id=theme)) streams=[] for match in re.finditer(rb'stream\r?\n(.*?)endstream',result.content,re.S): try: streams.append(zlib.decompress(base64.a85decode(match[1].strip().removesuffix(b'~>')))) except Exception: pass from reportlab.lib.rl_accel import fp_str command=(fp_str(*[int(palette[0][i:i+2],16)/255 for i in (1,3,5)])+' rg').encode() pages=[s for s in streams if b'BT' in s and b'/F' in s] assert len(pages)>1 assert all(command in s for s in pages) assert not any('浅色打印' in w for w in result.warnings) def test_pdf_accepts_asset_contract_beyond_previous_count_and_size(): assets=[png_asset(source=str(i)) for i in range(65)] assets[0]=assets[0].model_copy(update={'png_base64':'A'*2800004}) values=dict(source={'type':'markdown','markdown':'content'},assets=assets) ExportRequest(format='pdf',**values) with pytest.raises(ValidationError): ExportRequest(format='html',**values) with pytest.raises(ValidationError): ExportRequest(format='docx',**values) def test_pdf_large_png_still_requires_valid_format(): asset=png_asset((2100,2000)) assert validate_assets([asset],unlimited=True) with pytest.raises(Exception): validate_assets([asset]) with pytest.raises(Exception): validate_assets([asset.model_copy(update={'png_base64':'invalid'})],unlimited=True) def test_pdf_embeds_more_than_64_resources_with_theme_background(): doc=parse_document(('```mermaid\ngraph LR; A-->B\n```\n\n')*65) from app.export.assets import attach_assets attach_assets(doc,validate_assets([png_asset()],unlimited=True)) assert not enrich_document(doc,unlimited=True,options=ExportOptions(theme_id='dark')) assert all('static_png' in node.attributes for node in doc.children) with Image.open(BytesIO(doc.children[-1].attributes['static_png'])) as image: assert image.getpixel((0,0)) == (13,17,23) assert PdfExporter().render(doc,ExportOptions(theme_id='dark')).content.startswith(b'%PDF') def test_pdf_pipeline_ignores_source_and_output_quotas(monkeypatch): monkeypatch.setattr(service,'MAX_MARKDOWN_CHARS',8) monkeypatch.setattr(service,'MAX_EXPORT_BYTES',8) async def run(): job=await service.create_export(ExportRequest(format='pdf',source={'type':'markdown','markdown':'Beyond the previous quota.'})) done=await service.wait_for_export(job.job_id) assert done.status.value=='completed' assert service.get_export_file(job.job_id).stat().st_size>8 with pytest.raises(Exception): await service.create_export(ExportRequest(format='html',source={'type':'markdown','markdown':'Beyond the previous quota.'})) asyncio.run(run()) def test_pdf_accepts_more_than_16_curves_and_keeps_expression_safety(): doc=parse_document('```function-plot\n'+'\n'.join(f'y=x+{i}' for i in range(17))+'\n```') result=PdfExporter().render(doc,ExportOptions(theme_id='dark')) assert not any('函数图像' in w for w in result.warnings) unsafe=PdfExporter().render(parse_document('```function-plot\ny=__import__("os")\n```'),ExportOptions()) assert any('函数图像' in w for w in unsafe.warnings) def test_pdf_custom_palette_and_math_color(): palette=dict(zip(('page','surface','text','muted','code','border','accent'),PALETTES['midnight-purple'])) options=ExportOptions(theme_id='my-theme',palette=palette) doc=parse_document('Formula $x^2$') assert not enrich_document(doc,unlimited=True,options=options) math=next(n for n in doc.children[0].children if n.type=='math_inline') with Image.open(BytesIO(math.attributes['static_png'])) as image: assert image.getpixel((0,0))==(25,19,34) assert not any('主题' in w for w in PdfExporter().render(doc,options).warnings) with pytest.raises(ValidationError): ExportOptions(palette={**palette,'text':'url(file:///private)'})