pdf2epub 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,172 +0,0 @@
1
- from pathlib import Path
2
- import sys
3
- import json
4
- from PIL import Image
5
- import io
6
-
7
- def get_default_output_dir(input_path: Path) -> Path:
8
- """
9
- Generate default output directory path based on input PDF path.
10
- Creates a directory with same name as PDF (without extension) next to the PDF.
11
- """
12
- return input_path.parent / input_path.stem
13
-
14
- def get_default_input_dir() -> Path:
15
- """
16
- Get default input directory (./input) relative to current working directory.
17
- Creates it if it doesn't exist.
18
- """
19
- input_dir = Path.cwd() / 'input'
20
- input_dir.mkdir(exist_ok=True)
21
- return input_dir
22
-
23
-
24
- def save_images(images: dict, image_dir: Path) -> None:
25
- """
26
- Save images with proper error handling and format detection.
27
- Preserves original image filenames from the input.
28
-
29
- Args:
30
- images: Dictionary of images from marker-pdf conversion where keys are filenames
31
- image_dir: Directory to save images to
32
- """
33
- if not images:
34
- print("No images found in document")
35
- return
36
-
37
- image_dir.mkdir(exist_ok=True)
38
- saved_count = 0
39
-
40
- for filename, image_data in images.items():
41
- try:
42
- # Skip if image data is None or empty
43
- if not image_data:
44
- continue
45
-
46
- image_path = image_dir / filename
47
-
48
- # Handle different image data formats
49
- if isinstance(image_data, Image.Image):
50
- image_data.save(image_path)
51
- saved_count += 1
52
-
53
- elif isinstance(image_data, bytes):
54
- img = Image.open(io.BytesIO(image_data))
55
- img.save(image_path)
56
- saved_count += 1
57
-
58
- elif isinstance(image_data, str):
59
- if Path(image_data).exists():
60
- img = Image.open(image_data)
61
- img.save(image_path)
62
- saved_count += 1
63
- else:
64
- print(f"Image path does not exist: {image_data}")
65
- else:
66
- print(f"Unsupported image data type for {filename}: {type(image_data)}")
67
-
68
- except Exception as e:
69
- print(f"Error saving image {filename}: {str(e)}")
70
- continue
71
-
72
- if saved_count > 0:
73
- print(f"Successfully saved {saved_count} images to: {image_dir}")
74
- else:
75
- print("No valid images were found to save")
76
-
77
- def convert_pdf(
78
- input_path: str,
79
- output_dir: Path,
80
- max_pages: int = None,
81
- start_page: int = None,
82
- ) -> None:
83
- """
84
- Convert a single PDF file to markdown format with enhanced image handling.
85
- """
86
- try:
87
- from marker.models import create_model_dict
88
- from marker.converters.pdf import PdfConverter
89
-
90
- models = create_model_dict()
91
-
92
- # Build page_range config. marker-pdf 1.x requires an explicit list of
93
- # page indices; there is no "start to end" shorthand. If start_page is
94
- # given without max_pages we cannot construct the range without knowing
95
- # the document's page count, so we warn and process all pages instead.
96
- config = {}
97
- if max_pages is not None:
98
- s = start_page or 0
99
- config["page_range"] = list(range(s, s + max_pages))
100
- elif start_page is not None:
101
- print(
102
- "Warning: --start-page requires --max-pages in marker-pdf 1.x "
103
- "(no open-ended page range is supported). Processing all pages."
104
- )
105
-
106
- converter = PdfConverter(config=config, artifact_dict=models)
107
- rendered = converter(input_path)
108
-
109
- full_text = rendered.markdown
110
- images = rendered.images
111
- metadata = rendered.metadata
112
-
113
- # All output will go to the output directory
114
- output_dir.mkdir(parents=True, exist_ok=True)
115
-
116
- # Save markdown content
117
- md_output = output_dir / f"{Path(input_path).stem}.md"
118
- md_output.write_text(full_text, encoding='utf-8')
119
- print(f"Markdown saved to: {md_output}")
120
-
121
- # Save metadata as JSON
122
- meta_output = output_dir / f"{Path(input_path).stem}_metadata.json"
123
- with open(meta_output, 'w', encoding='utf-8') as f:
124
- json.dump(metadata, f, indent=2)
125
- print(f"Metadata saved to: {meta_output}")
126
-
127
- # Enhanced image handling
128
- try:
129
- if images:
130
- image_dir = output_dir / "images"
131
- save_images(images, image_dir)
132
-
133
- # Cleanup PIL Images
134
- for img in images.values():
135
- if isinstance(img, Image.Image):
136
- try:
137
- img.close()
138
- except Exception as e:
139
- print(f"Warning: Failed to close image: {e}")
140
- images.clear()
141
- except Exception as e:
142
- print(f"Warning: Error during image cleanup: {e}")
143
-
144
- except Exception as e:
145
- print(f"Error converting {input_path}: {str(e)}", file=sys.stderr)
146
- raise
147
-
148
-
149
- def add_pdfs_to_queue(input_path: Path) -> list[Path]:
150
- """
151
- Add PDF files to the processing queue.
152
- If input_path is a directory, add all PDFs in it.
153
- If input_path is a file, add just that file.
154
- """
155
- queue = []
156
-
157
- if input_path.is_dir():
158
- pdfs = list(input_path.glob('*.pdf'))
159
- if not pdfs:
160
- print(f"No PDF files found in directory: {input_path}", file=sys.stderr)
161
- sys.exit(1)
162
- queue.extend(pdfs)
163
- else:
164
- if not input_path.is_file():
165
- print(f"Error: Input file does not exist: {input_path}", file=sys.stderr)
166
- sys.exit(1)
167
- if input_path.suffix.lower() != '.pdf':
168
- print(f"Error: Input file must be a PDF: {input_path}", file=sys.stderr)
169
- sys.exit(1)
170
- queue.append(input_path)
171
-
172
- return queue
@@ -1,41 +0,0 @@
1
- You are a specialized pattern recognition system for markdown documents generated from PDF conversions. Your task is to analyze the following markdown content for common error patterns that typically occur during PDF-to-markdown conversion, while adhering to these strict requirements:
2
-
3
- 1. DO NOT alter any markdown formatting syntax
4
- 2. DO NOT modify any actual content
5
- 3. DO NOT suggest changes to filenames or file structure
6
- 4. ONLY identify patterns that represent clear conversion errors
7
-
8
- Analyze the markdown for patterns such as:
9
- - Incorrectly joined words (e.g., "helloworld" instead of "hello world")
10
- - Misplaced line breaks that split sentences inappropriately
11
- - Repeated punctuation marks or whitespace
12
- - Common OCR artifacts (e.g., "rn" being recognized as "m")
13
- - Inconsistent list item formatting
14
- - Table formatting inconsistencies
15
- - Code block formatting issues
16
- - Image reference errors
17
- - Header hierarchy inconsistencies
18
- - Footnote formatting issues
19
-
20
- PROVIDE YOUR RESPONSE ONLY AS A JSON OBJECT WITH THIS STRUCTURE:
21
- {
22
- "patterns": [
23
- {
24
- "pattern_id": "unique_identifier",
25
- "description": "Brief description of the error pattern",
26
- "regex": "regex_pattern_to_match_error",
27
- "severity": "high|medium|low",
28
- "category": "formatting|ocr|structure|whitespace|other",
29
- "example": "Example of the pattern from the text"
30
- }
31
- ]
32
- }
33
-
34
- The regex patterns must:
35
- 1. Use Python regex syntax
36
- 2. Include word boundaries where appropriate
37
- 3. Be as specific as possible to avoid false positives
38
- 4. Account for possible variations in the pattern
39
- 5. Be tested against the provided content
40
-
41
- DO NOT include any explanatory text or additional information outside the JSON structure.
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- PDF2EPUB Markdown Postprocessing Template
4
-
5
- This template is used to analyze markdown content and generate a customized
6
- postprocessing script that fixes common error patterns while preserving
7
- markdown formatting and content integrity.
8
-
9
- Instructions for AI:
10
- 1. Analyze the provided markdown section for common error patterns
11
- 2. Define regex patterns to identify these errors
12
- 3. Provide safe replacement patterns that preserve markdown formatting
13
- 4. Return a complete script with the patterns and fixes
14
-
15
- IMPORTANT:
16
- - Do not modify actual content or meaning
17
- - Preserve all markdown formatting
18
- - Ensure filename and location remain unchanged
19
- - Validate all regex patterns before replacing
20
- - Use non-destructive transformations only
21
-
22
- Send ONLY the python script content back to the user. your answer should exclusively contain the script which can be directly run by the user and have the input filename and the output filename with the appropriate absolute path hard-coded.
23
- """
24
-
25
- import re
26
- from pathlib import Path
27
- from typing import List
28
- import logging
29
-
30
- # Configure logging
31
- logging.basicConfig(
32
- level=logging.INFO,
33
- format='%(asctime)s - %(levelname)s - %(message)s'
34
- )
35
- logger = logging.getLogger(__name__)
36
-
37
- class MarkdownFix:
38
- """Represents a specific markdown formatting fix with its regex pattern."""
39
- def __init__(self, name: str, pattern: str, replacement: str, description: str):
40
- self.name = name
41
- self.pattern = pattern
42
- self.replacement = replacement
43
- self.description = description
44
-
45
- def apply(self, content: str) -> str:
46
- """Apply the fix while preserving markdown formatting."""
47
- try:
48
- # Compile regex pattern
49
- regex = re.compile(self.pattern, re.MULTILINE | re.DOTALL)
50
-
51
- # Count matches before replacement
52
- matches = len(regex.findall(content))
53
-
54
- # Apply fix
55
- new_content = regex.sub(self.replacement, content)
56
-
57
- # Log the changes
58
- if matches > 0:
59
- logger.info(f"Applied fix '{self.name}': {matches} matches found and processed")
60
-
61
- return new_content
62
-
63
- except re.error as e:
64
- logger.error(f"Regex error in fix '{self.name}': {str(e)}")
65
- return content
66
- except Exception as e:
67
- logger.error(f"Error applying fix '{self.name}': {str(e)}")
68
- return content
69
-
70
- class MarkdownPostprocessor:
71
- """Handles the postprocessing of markdown files."""
72
-
73
- def __init__(self):
74
- self.fixes: List[MarkdownFix] = []
75
-
76
- def add_fix(self, fix: MarkdownFix) -> None:
77
- """Add a new fix to the processor."""
78
- self.fixes.append(fix)
79
-
80
- def validate_content(self, content: str) -> bool:
81
- """
82
- Validate that the content still contains essential markdown elements
83
- after processing.
84
- """
85
- # Check for common markdown structures that should be preserved
86
- essential_patterns = [
87
- (r'^#+ .*$', 'headers'), # Headers
88
- (r'^\* .*$', 'bullet points'), # Bullet points
89
- (r'^\d+\. .*$', 'numbered lists'), # Numbered lists
90
- (r'`[^`]+`', 'inline code'), # Inline code
91
- (r'^\s*```.*?```', 'code blocks'), # Code blocks
92
- (r'!\[.*?\]\(.*?\)', 'images'), # Images
93
- (r'\[.*?\]\(.*?\)', 'links'), # Links
94
- (r'\*\*.*?\*\*', 'bold text'), # Bold text
95
- (r'_.*?_', 'italic text'), # Italic text
96
- (r'\|.*?\|', 'tables'), # Tables
97
- ]
98
-
99
- # Store counts before processing
100
- initial_counts = {
101
- name: len(re.findall(pattern, content, re.MULTILINE | re.DOTALL))
102
- for pattern, name in essential_patterns
103
- }
104
-
105
- return initial_counts
106
-
107
- def process_file(self, filepath: Path) -> bool:
108
- """
109
- Process a markdown file while preserving its formatting and content.
110
- Returns True if processing was successful.
111
- """
112
- try:
113
- # Read original content
114
- original_content = filepath.read_text(encoding='utf-8')
115
-
116
- # Validate initial content
117
- initial_counts = self.validate_content(original_content)
118
-
119
- # Create temporary content for processing
120
- processed_content = original_content
121
-
122
- # Apply each fix sequentially
123
- for fix in self.fixes:
124
- processed_content = fix.apply(processed_content)
125
-
126
- # Validate processed content
127
- final_counts = self.validate_content(processed_content)
128
-
129
- # Compare markdown element counts
130
- for element, initial_count in initial_counts.items():
131
- final_count = final_counts[element]
132
- if final_count < initial_count:
133
- logger.warning(
134
- f"Warning: Number of {element} decreased from "
135
- f"{initial_count} to {final_count}"
136
- )
137
-
138
- # Write processed content back to the same file
139
- filepath.write_text(processed_content, encoding='utf-8')
140
-
141
- logger.info(f"Successfully processed: {filepath}")
142
- return True
143
-
144
- except Exception as e:
145
- logger.error(f"Error processing {filepath}: {str(e)}")
146
- return False
147
-
148
- def main():
149
- """
150
- AI INSTRUCTIONS:
151
- Replace this function with specific fixes for the analyzed markdown content.
152
- Include regex patterns and replacements that address identified issues.
153
- """
154
- processor = MarkdownPostprocessor()
155
-
156
- # Example fixes (AI should replace these with document-specific fixes):
157
- processor.add_fix(MarkdownFix(
158
- name="fix_excessive_newlines",
159
- pattern=r"\n{3,}",
160
- replacement="\n\n",
161
- description="Replace excessive newlines with double newlines"
162
- ))
163
-
164
- # Add more fixes here based on analysis...
165
-
166
- return processor
167
-
168
- if __name__ == "__main__":
169
- # Get processor with document-specific fixes
170
- processor = main()
171
-
172
- # Process the markdown file
173
- input_file = Path("input.md") # AI should use actual filename
174
- if processor.process_file(input_file):
175
- logger.info("Postprocessing completed successfully")
176
- else:
177
- logger.error("Postprocessing failed")
@@ -1,8 +0,0 @@
1
- # Python 3.13 is recommended.
2
- # marker-pdf constrains Pillow<11.0.0, and Pillow only ships cp314 wheels from
3
- # 11.3.0 onward, so on Python 3.14 pip builds Pillow from source rather than
4
- # installing a wheel.
5
- marker-pdf==1.10.2
6
- transformers==4.57.6
7
- markdown==3.10.2
8
- latex2mathml==3.81.0