Coverage for src/local_deep_research/exporters/pdf_exporter.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-03 23:15 +0000

1"""PDF export service wrapper. 

2 

3This module provides the PDFExporter class that wraps the existing 

4PDFService implementation from web/services/pdf_service.py. 

5 

6The existing PDFService is kept unchanged - this is just a wrapper 

7to integrate it with the modular exporter registry. 

8""" 

9 

10from typing import Optional 

11 

12from loguru import logger 

13 

14from ..web.services.pdf_service import get_pdf_service 

15from .base import BaseExporter, ExportOptions, ExportResult 

16from .registry import ExporterRegistry 

17 

18 

19@ExporterRegistry.register 

20class PDFExporter(BaseExporter): 

21 """PDF exporter wrapping the existing PDFService implementation. 

22 

23 This exporter delegates to the existing PDFService which uses 

24 WeasyPrint for PDF generation. No changes are made to the 

25 underlying PDF generation logic. 

26 """ 

27 

28 def __init__(self): 

29 """Initialize PDF exporter. 

30 

31 Note: pdf_service import is deferred to export() so that 

32 registration succeeds even when WeasyPrint system libraries 

33 (Pango, Cairo, GLib) are not installed. 

34 """ 

35 

36 @property 

37 def format_name(self) -> str: 

38 return "pdf" 

39 

40 @property 

41 def file_extension(self) -> str: 

42 return ".pdf" 

43 

44 @property 

45 def mimetype(self) -> str: 

46 return "application/pdf" 

47 

48 def export( 

49 self, 

50 markdown_content: str, 

51 options: Optional[ExportOptions] = None, 

52 ) -> ExportResult: 

53 """Convert markdown content to PDF using existing PDFService. 

54 

55 Args: 

56 markdown_content: The markdown text to convert 

57 options: Optional export options (title, metadata, custom_css) 

58 

59 Returns: 

60 ExportResult with PDF content as bytes, filename, and mimetype 

61 

62 Raises: 

63 ValueError: If content exceeds maximum size limit 

64 """ 

65 try: 

66 # Check content size limit to prevent OOM errors 

67 self._validate_content_size(markdown_content) 

68 

69 pdf_service = get_pdf_service() 

70 

71 options = options or ExportOptions() 

72 

73 # Prepend title if needed (for document formats like PDF) 

74 markdown_content = self._prepend_title_if_needed( 

75 markdown_content, options.title 

76 ) 

77 

78 # Extract custom_css if provided 

79 custom_css = None 

80 if options.custom_options: 

81 custom_css = options.custom_options.get("custom_css") 

82 

83 # Delegate to existing PDFService 

84 pdf_bytes = pdf_service.markdown_to_pdf( 

85 markdown_content, 

86 title=options.title, 

87 metadata=options.metadata, 

88 custom_css=custom_css, 

89 ) 

90 

91 filename = self._generate_safe_filename(options.title) 

92 

93 logger.info( 

94 f"Generated PDF in memory, size: {len(pdf_bytes)} bytes" 

95 ) 

96 

97 return ExportResult( 

98 content=pdf_bytes, 

99 filename=filename, 

100 mimetype=self.mimetype, 

101 ) 

102 

103 except Exception: 

104 logger.exception("Error generating PDF") 

105 raise