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.
- package/README.md +35 -121
- package/package.json +9 -44
- package/pdf2epub.js +624 -0
- package/LICENSE +0 -22
- package/bin/pdf2epub.js +0 -14
- package/lib/cli.js +0 -134
- package/lib/paths.js +0 -44
- package/lib/python.js +0 -76
- package/lib/setup.js +0 -152
- package/vendor/pdf2epub/LICENSE +0 -9
- package/vendor/pdf2epub/README.md +0 -217
- package/vendor/pdf2epub/UPSTREAM.json +0 -8
- package/vendor/pdf2epub/main.py +0 -118
- package/vendor/pdf2epub/modules/__init__.py +0 -0
- package/vendor/pdf2epub/modules/mark2epub.py +0 -696
- package/vendor/pdf2epub/modules/pdf2md.py +0 -172
- package/vendor/pdf2epub/modules/postprocessing/__init__.py +0 -0
- package/vendor/pdf2epub/modules/postprocessing/prompt.txt +0 -41
- package/vendor/pdf2epub/modules/postprocessing/template.py +0 -177
- package/vendor/pdf2epub/requirements.txt +0 -8
|
@@ -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
|
|
File without changes
|
|
@@ -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
|