Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
@@ -20,6 +20,8 @@ from reportlab.pdfbase import pdfmetrics
|
|
20 |
from reportlab.pdfbase.ttfonts import TTFont
|
21 |
|
22 |
# --- Configuration & Setup ---
|
|
|
|
|
23 |
LAYOUTS = {
|
24 |
"A4 Portrait": {"size": A4},
|
25 |
"A4 Landscape": {"size": landscape(A4)},
|
@@ -28,9 +30,9 @@ LAYOUTS = {
|
|
28 |
"Legal Portrait": {"size": legal},
|
29 |
"Legal Landscape": {"size": landscape(legal)},
|
30 |
}
|
31 |
-
OUTPUT_DIR =
|
32 |
-
PREVIEW_DIR =
|
33 |
-
FONT_DIR =
|
34 |
EMOJI_FONT_NAME = "NotoColorEmoji"
|
35 |
|
36 |
# Create necessary directories
|
@@ -43,6 +45,7 @@ FONT_DIR.mkdir(exist_ok=True)
|
|
43 |
|
44 |
def download_and_register_fonts():
|
45 |
"""Downloads default fonts if needed, then finds and registers all .ttf files."""
|
|
|
46 |
fonts_to_download = {
|
47 |
"DejaVuSans.ttf": "https://github.com/dejavu-fonts/dejavu-fonts/blob/main/ttf/DejaVuSans.ttf?raw=true",
|
48 |
"NotoColorEmoji.ttf": "https://github.com/googlefonts/noto-emoji/blob/main/fonts/NotoColorEmoji.ttf?raw=true"
|
@@ -60,14 +63,17 @@ def download_and_register_fonts():
|
|
60 |
print(f"Failed to download {filename}: {e}")
|
61 |
|
62 |
font_names = []
|
63 |
-
for
|
|
|
|
|
|
|
|
|
64 |
try:
|
65 |
font_name = font_path.stem
|
|
|
66 |
pdfmetrics.registerFont(TTFont(font_name, str(font_path)))
|
67 |
|
68 |
# CRITICAL FIX for bold/italic error:
|
69 |
-
# Register dummy bold/italic variants for all fonts, including emoji fonts.
|
70 |
-
# ReportLab will use these fallbacks instead of crashing if a true variant is not found.
|
71 |
pdfmetrics.registerFont(TTFont(f"{font_name}-Bold", str(font_path)))
|
72 |
pdfmetrics.registerFont(TTFont(f"{font_name}-Italic", str(font_path)))
|
73 |
pdfmetrics.registerFont(TTFont(f"{font_name}-BoldItalic", str(font_path)))
|
@@ -77,6 +83,8 @@ def download_and_register_fonts():
|
|
77 |
font_names.append(font_name)
|
78 |
except Exception as e:
|
79 |
print(f"Could not register font {font_path.name}: {e}")
|
|
|
|
|
80 |
return sorted(font_names)
|
81 |
|
82 |
def apply_emoji_font(text: str) -> str:
|
@@ -93,20 +101,17 @@ def apply_emoji_font(text: str) -> str:
|
|
93 |
|
94 |
def markdown_to_story(markdown_text: str, font_name: str):
|
95 |
"""Converts markdown to a ReportLab story, handling emojis and page breaks."""
|
96 |
-
# This function remains largely the same but relies on the global font registration fix.
|
97 |
styles = getSampleStyleSheet()
|
98 |
style_normal = ParagraphStyle('BodyText', fontName=font_name, spaceAfter=6, leading=14)
|
99 |
style_h1 = ParagraphStyle('h1', parent=styles['h1'], fontName=font_name, spaceBefore=12, fontSize=20, leading=24)
|
100 |
style_code = ParagraphStyle('Code', fontName='Courier', backColor=colors.whitesmoke)
|
101 |
|
102 |
story = []
|
103 |
-
# Process content line-by-line
|
104 |
for line in markdown_text.split('\n'):
|
105 |
line_with_emoji = apply_emoji_font(line)
|
106 |
formatted_line = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', line_with_emoji))
|
107 |
|
108 |
if line.startswith("# "): story.append(Paragraph(formatted_line[2:], style_h1))
|
109 |
-
# ... other markdown elements would be handled similarly
|
110 |
else: story.append(Paragraph(formatted_line, style_normal))
|
111 |
return story
|
112 |
|
@@ -119,7 +124,7 @@ def create_pdf_preview(pdf_path: Path):
|
|
119 |
pix = page.get_pixmap()
|
120 |
pix.save(str(preview_path))
|
121 |
doc.close()
|
122 |
-
return preview_path
|
123 |
except Exception as e:
|
124 |
print(f"Could not create preview for {pdf_path.name}: {e}")
|
125 |
return None
|
@@ -137,7 +142,6 @@ def generate_pdfs_api(files, layouts, fonts, num_columns, progress=gr.Progress(t
|
|
137 |
OUTPUT_DIR.mkdir()
|
138 |
PREVIEW_DIR.mkdir()
|
139 |
|
140 |
-
# Group files by a common stem (e.g., AARON_1.md and AARON_2.png are grouped under "AARON")
|
141 |
grouped_files = defaultdict(lambda: {'md': [], 'img': []})
|
142 |
for f in files:
|
143 |
file_path = Path(f.name)
|
@@ -155,7 +159,6 @@ def generate_pdfs_api(files, layouts, fonts, num_columns, progress=gr.Progress(t
|
|
155 |
for font_name in fonts:
|
156 |
merger = PdfWriter()
|
157 |
|
158 |
-
# --- Process Markdown ---
|
159 |
if assets['md']:
|
160 |
md_content = "\n\n---PAGE_BREAK---\n\n".join([p.read_text(encoding='utf-8') for p in assets['md']])
|
161 |
md_buffer = io.BytesIO()
|
@@ -173,18 +176,14 @@ def generate_pdfs_api(files, layouts, fonts, num_columns, progress=gr.Progress(t
|
|
173 |
doc.build(story)
|
174 |
merger.append(fileobj=md_buffer)
|
175 |
|
176 |
-
# --- Process Images ---
|
177 |
for img_path in assets['img']:
|
178 |
with Image.open(img_path) as img:
|
179 |
img_width, img_height = img.size
|
180 |
-
|
181 |
img_buffer = io.BytesIO()
|
182 |
-
# Page size is determined by the image itself
|
183 |
doc = SimpleDocTemplate(img_buffer, pagesize=(img_width, img_height), leftMargin=0, rightMargin=0, topMargin=0, bottomMargin=0)
|
184 |
doc.build([ReportLabImage(img_path, width=img_width, height=img_height)])
|
185 |
merger.append(fileobj=img_buffer)
|
186 |
|
187 |
-
# --- Save Merged PDF ---
|
188 |
if len(merger.pages) > 0:
|
189 |
time_str = datetime.datetime.now().strftime('%m-%d-%a_%I%M%p').upper()
|
190 |
filename = f"{stem}_{time_str}_{layout_name.replace(' ','-')}_{font_name}_Cols{num_columns}.pdf"
|
@@ -194,16 +193,15 @@ def generate_pdfs_api(files, layouts, fonts, num_columns, progress=gr.Progress(t
|
|
194 |
generated_pdf_paths.append(output_path)
|
195 |
log_updates += f"Generated: {filename}\n"
|
196 |
|
197 |
-
# --- Create Previews for Gallery ---
|
198 |
gallery_previews = [create_pdf_preview(p) for p in generated_pdf_paths]
|
199 |
final_gallery = [g for g in gallery_previews if g is not None]
|
200 |
|
201 |
-
return final_gallery, log_updates
|
202 |
|
203 |
# --- Gradio UI Definition ---
|
204 |
AVAILABLE_FONTS = download_and_register_fonts()
|
205 |
SAMPLE_MARKDOWN = "# Sample Document π\n\nThis shows **bold**, _italic_, and emojis like π and π».\n\n### A Table\n| Flavor | Rating |\n|---|---|\n| Chocolate π«| 10/10 |\n| Vanilla π¦| 9/10 |\n\n```python\n# Code blocks too!\ndef hello():\n return 'Hello from PDF!'\n```"
|
206 |
-
with open("sample.md", "w") as f: f.write(SAMPLE_MARKDOWN)
|
207 |
|
208 |
with gr.Blocks(theme=gr.themes.Soft(), title="Advanced PDF Generator") as demo:
|
209 |
gr.Markdown("# π Advanced PDF Generator")
|
@@ -212,7 +210,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Advanced PDF Generator") as demo:
|
|
212 |
with gr.Row():
|
213 |
with gr.Column(scale=1):
|
214 |
gr.Markdown("### βοΈ Generation Settings")
|
215 |
-
uploaded_files = gr.File(label="Upload Markdown & Image Files", file_count="multiple", file_types=[".md", ".png", ".jpg"])
|
216 |
num_columns_slider = gr.Slider(label="Number of Columns for Text", minimum=1, maximum=4, step=1, value=1)
|
217 |
selected_layouts = gr.CheckboxGroup(choices=list(LAYOUTS.keys()), label="Select Page Layouts", value=["A4 Portrait"])
|
218 |
selected_fonts = gr.CheckboxGroup(choices=AVAILABLE_FONTS, label="Select Text Fonts", value=[AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else [])
|
@@ -222,16 +220,21 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Advanced PDF Generator") as demo:
|
|
222 |
gr.Markdown("### πΌοΈ PDF Preview Gallery")
|
223 |
gallery_output = gr.Gallery(label="Generated PDF Previews", show_label=False, elem_id="gallery", columns=3, height="auto")
|
224 |
log_output = gr.Markdown(label="Generation Log", value="Logs will appear here...")
|
|
|
225 |
|
226 |
-
def auto_run_demo():
|
227 |
"""Function to run on app load to generate a sample PDF."""
|
228 |
print("Running initial demo generation...")
|
229 |
-
|
230 |
-
|
231 |
-
|
|
|
|
|
|
|
|
|
232 |
|
233 |
-
demo.load(auto_run_demo, inputs=None, outputs=[gallery_output, log_output])
|
234 |
-
generate_btn.click(fn=generate_pdfs_api, inputs=[uploaded_files, selected_layouts, selected_fonts, num_columns_slider], outputs=[gallery_output, log_output])
|
235 |
|
236 |
if __name__ == "__main__":
|
237 |
demo.launch()
|
|
|
20 |
from reportlab.pdfbase.ttfonts import TTFont
|
21 |
|
22 |
# --- Configuration & Setup ---
|
23 |
+
# Use the current working directory as the base for all paths for robustness.
|
24 |
+
CWD = Path.cwd()
|
25 |
LAYOUTS = {
|
26 |
"A4 Portrait": {"size": A4},
|
27 |
"A4 Landscape": {"size": landscape(A4)},
|
|
|
30 |
"Legal Portrait": {"size": legal},
|
31 |
"Legal Landscape": {"size": landscape(legal)},
|
32 |
}
|
33 |
+
OUTPUT_DIR = CWD / "generated_pdfs"
|
34 |
+
PREVIEW_DIR = CWD / "previews"
|
35 |
+
FONT_DIR = CWD / "fonts"
|
36 |
EMOJI_FONT_NAME = "NotoColorEmoji"
|
37 |
|
38 |
# Create necessary directories
|
|
|
45 |
|
46 |
def download_and_register_fonts():
|
47 |
"""Downloads default fonts if needed, then finds and registers all .ttf files."""
|
48 |
+
print("--- Font Registration Process Starting ---")
|
49 |
fonts_to_download = {
|
50 |
"DejaVuSans.ttf": "https://github.com/dejavu-fonts/dejavu-fonts/blob/main/ttf/DejaVuSans.ttf?raw=true",
|
51 |
"NotoColorEmoji.ttf": "https://github.com/googlefonts/noto-emoji/blob/main/fonts/NotoColorEmoji.ttf?raw=true"
|
|
|
63 |
print(f"Failed to download {filename}: {e}")
|
64 |
|
65 |
font_names = []
|
66 |
+
print(f"Scanning for fonts in: {FONT_DIR.absolute()}")
|
67 |
+
font_files = list(FONT_DIR.glob("*.ttf"))
|
68 |
+
print(f"Found {len(font_files)} .ttf files: {[f.name for f in font_files]}")
|
69 |
+
|
70 |
+
for font_path in font_files:
|
71 |
try:
|
72 |
font_name = font_path.stem
|
73 |
+
print(f"Registering font: '{font_name}' from {font_path.name}")
|
74 |
pdfmetrics.registerFont(TTFont(font_name, str(font_path)))
|
75 |
|
76 |
# CRITICAL FIX for bold/italic error:
|
|
|
|
|
77 |
pdfmetrics.registerFont(TTFont(f"{font_name}-Bold", str(font_path)))
|
78 |
pdfmetrics.registerFont(TTFont(f"{font_name}-Italic", str(font_path)))
|
79 |
pdfmetrics.registerFont(TTFont(f"{font_name}-BoldItalic", str(font_path)))
|
|
|
83 |
font_names.append(font_name)
|
84 |
except Exception as e:
|
85 |
print(f"Could not register font {font_path.name}: {e}")
|
86 |
+
print(f"Successfully registered user-selectable fonts: {font_names}")
|
87 |
+
print("--- Font Registration Process Finished ---")
|
88 |
return sorted(font_names)
|
89 |
|
90 |
def apply_emoji_font(text: str) -> str:
|
|
|
101 |
|
102 |
def markdown_to_story(markdown_text: str, font_name: str):
|
103 |
"""Converts markdown to a ReportLab story, handling emojis and page breaks."""
|
|
|
104 |
styles = getSampleStyleSheet()
|
105 |
style_normal = ParagraphStyle('BodyText', fontName=font_name, spaceAfter=6, leading=14)
|
106 |
style_h1 = ParagraphStyle('h1', parent=styles['h1'], fontName=font_name, spaceBefore=12, fontSize=20, leading=24)
|
107 |
style_code = ParagraphStyle('Code', fontName='Courier', backColor=colors.whitesmoke)
|
108 |
|
109 |
story = []
|
|
|
110 |
for line in markdown_text.split('\n'):
|
111 |
line_with_emoji = apply_emoji_font(line)
|
112 |
formatted_line = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', line_with_emoji))
|
113 |
|
114 |
if line.startswith("# "): story.append(Paragraph(formatted_line[2:], style_h1))
|
|
|
115 |
else: story.append(Paragraph(formatted_line, style_normal))
|
116 |
return story
|
117 |
|
|
|
124 |
pix = page.get_pixmap()
|
125 |
pix.save(str(preview_path))
|
126 |
doc.close()
|
127 |
+
return str(preview_path)
|
128 |
except Exception as e:
|
129 |
print(f"Could not create preview for {pdf_path.name}: {e}")
|
130 |
return None
|
|
|
142 |
OUTPUT_DIR.mkdir()
|
143 |
PREVIEW_DIR.mkdir()
|
144 |
|
|
|
145 |
grouped_files = defaultdict(lambda: {'md': [], 'img': []})
|
146 |
for f in files:
|
147 |
file_path = Path(f.name)
|
|
|
159 |
for font_name in fonts:
|
160 |
merger = PdfWriter()
|
161 |
|
|
|
162 |
if assets['md']:
|
163 |
md_content = "\n\n---PAGE_BREAK---\n\n".join([p.read_text(encoding='utf-8') for p in assets['md']])
|
164 |
md_buffer = io.BytesIO()
|
|
|
176 |
doc.build(story)
|
177 |
merger.append(fileobj=md_buffer)
|
178 |
|
|
|
179 |
for img_path in assets['img']:
|
180 |
with Image.open(img_path) as img:
|
181 |
img_width, img_height = img.size
|
|
|
182 |
img_buffer = io.BytesIO()
|
|
|
183 |
doc = SimpleDocTemplate(img_buffer, pagesize=(img_width, img_height), leftMargin=0, rightMargin=0, topMargin=0, bottomMargin=0)
|
184 |
doc.build([ReportLabImage(img_path, width=img_width, height=img_height)])
|
185 |
merger.append(fileobj=img_buffer)
|
186 |
|
|
|
187 |
if len(merger.pages) > 0:
|
188 |
time_str = datetime.datetime.now().strftime('%m-%d-%a_%I%M%p').upper()
|
189 |
filename = f"{stem}_{time_str}_{layout_name.replace(' ','-')}_{font_name}_Cols{num_columns}.pdf"
|
|
|
193 |
generated_pdf_paths.append(output_path)
|
194 |
log_updates += f"Generated: {filename}\n"
|
195 |
|
|
|
196 |
gallery_previews = [create_pdf_preview(p) for p in generated_pdf_paths]
|
197 |
final_gallery = [g for g in gallery_previews if g is not None]
|
198 |
|
199 |
+
return final_gallery, log_updates, [str(p) for p in generated_pdf_paths]
|
200 |
|
201 |
# --- Gradio UI Definition ---
|
202 |
AVAILABLE_FONTS = download_and_register_fonts()
|
203 |
SAMPLE_MARKDOWN = "# Sample Document π\n\nThis shows **bold**, _italic_, and emojis like π and π».\n\n### A Table\n| Flavor | Rating |\n|---|---|\n| Chocolate π«| 10/10 |\n| Vanilla π¦| 9/10 |\n\n```python\n# Code blocks too!\ndef hello():\n return 'Hello from PDF!'\n```"
|
204 |
+
with open(CWD / "sample.md", "w") as f: f.write(SAMPLE_MARKDOWN)
|
205 |
|
206 |
with gr.Blocks(theme=gr.themes.Soft(), title="Advanced PDF Generator") as demo:
|
207 |
gr.Markdown("# π Advanced PDF Generator")
|
|
|
210 |
with gr.Row():
|
211 |
with gr.Column(scale=1):
|
212 |
gr.Markdown("### βοΈ Generation Settings")
|
213 |
+
uploaded_files = gr.File(label="Upload Markdown & Image Files", file_count="multiple", file_types=[".md", ".png", ".jpg", ".jpeg"])
|
214 |
num_columns_slider = gr.Slider(label="Number of Columns for Text", minimum=1, maximum=4, step=1, value=1)
|
215 |
selected_layouts = gr.CheckboxGroup(choices=list(LAYOUTS.keys()), label="Select Page Layouts", value=["A4 Portrait"])
|
216 |
selected_fonts = gr.CheckboxGroup(choices=AVAILABLE_FONTS, label="Select Text Fonts", value=[AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else [])
|
|
|
220 |
gr.Markdown("### πΌοΈ PDF Preview Gallery")
|
221 |
gallery_output = gr.Gallery(label="Generated PDF Previews", show_label=False, elem_id="gallery", columns=3, height="auto")
|
222 |
log_output = gr.Markdown(label="Generation Log", value="Logs will appear here...")
|
223 |
+
downloadable_files_output = gr.Files(label="Download Generated PDFs")
|
224 |
|
225 |
+
def auto_run_demo(request: gr.Request):
|
226 |
"""Function to run on app load to generate a sample PDF."""
|
227 |
print("Running initial demo generation...")
|
228 |
+
# Create a dummy file object for Gradio
|
229 |
+
with open(CWD / "sample.md", "rb") as f:
|
230 |
+
sample_bytes = f.read()
|
231 |
+
|
232 |
+
demo_files = [gr.processing_utils.SavedFile(name=str(CWD / "sample.md"), data=sample_bytes, is_file=True)]
|
233 |
+
previews, logs, files = generate_pdfs_api(demo_files, ["A4 Portrait"], [AVAILABLE_FONTS[0]] if AVAILABLE_FONTS else [], 1)
|
234 |
+
return previews, logs, files
|
235 |
|
236 |
+
demo.load(auto_run_demo, inputs=None, outputs=[gallery_output, log_output, downloadable_files_output])
|
237 |
+
generate_btn.click(fn=generate_pdfs_api, inputs=[uploaded_files, selected_layouts, selected_fonts, num_columns_slider], outputs=[gallery_output, log_output, downloadable_files_output])
|
238 |
|
239 |
if __name__ == "__main__":
|
240 |
demo.launch()
|