97 lines
4.9 KiB
Python
97 lines
4.9 KiB
Python
"""PDF theme and resource policy regressions; no real providers or user files."""
|
|
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)'})
|