davidberenstein1957 HF staff commited on
Commit
eddcd94
·
unverified ·
1 Parent(s): 24ed042

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +347 -0
main.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import List, Literal, Optional, Tuple, Union
6
+ from urllib.parse import urlparse
7
+
8
+ import cairosvg
9
+ import numpy as np
10
+ import pygments
11
+ import requests
12
+ from PIL import Image, ImageDraw, ImageFont
13
+ from pygments.formatters import ImageFormatter
14
+ from pygments.lexers import PythonLexer
15
+ from pygments.styles import get_style_by_name
16
+
17
+
18
+ def create_gradient_background(
19
+ width: int,
20
+ height: int,
21
+ start_color: Tuple[int, int, int],
22
+ end_color: Tuple[int, int, int],
23
+ frame_num: int = 0,
24
+ ) -> Image.Image:
25
+ """Create animated gradient background with wave effects."""
26
+ # Pre-calculate wave patterns using numpy operations
27
+ x = np.arange(width)
28
+ y = np.arange(height)
29
+ X, Y = np.meshgrid(x, y)
30
+
31
+ wave1 = 30 * np.sin(Y * 0.02 + frame_num * 0.1)
32
+ wave2 = 15 * np.sin(Y * 0.03 - frame_num * 0.05)
33
+ wave3 = 10 * np.cos(X * 0.02 + frame_num * 0.08)
34
+ wave = wave1 + wave2 + wave3
35
+
36
+ # Vectorized calculation of progress values
37
+ base_progress = Y / height
38
+ wave_offset = wave / height
39
+ progress = np.clip(base_progress + wave_offset, 0, 1)
40
+
41
+ # Vectorized color interpolation
42
+ r = np.clip(
43
+ (start_color[0] + (end_color[0] - start_color[0]) * progress), 0, 255
44
+ ).astype(np.uint8)
45
+ g = np.clip(
46
+ (start_color[1] + (end_color[1] - start_color[1]) * progress), 0, 255
47
+ ).astype(np.uint8)
48
+ b = np.clip(
49
+ (start_color[2] + (end_color[2] - start_color[2]) * progress), 0, 255
50
+ ).astype(np.uint8)
51
+
52
+ # Create RGB array and convert to image
53
+ rgb_array = np.stack([r, g, b], axis=2)
54
+ return Image.fromarray(rgb_array)
55
+
56
+
57
+ def create_window_background(
58
+ width: int,
59
+ height: int,
60
+ style_name: str,
61
+ filename: Optional[str] = None,
62
+ font_size: int = 24,
63
+ ) -> Image.Image:
64
+ """Create window background with title bar and control buttons."""
65
+ # Get background color from style
66
+ style_obj = get_style_by_name(style_name)
67
+ bg_color = style_obj.background_color
68
+ if bg_color.startswith("#"):
69
+ bg_color = tuple(int(bg_color[i : i + 2], 16) for i in (1, 3, 5))
70
+ else:
71
+ bg_color = (40, 40, 40) # Default dark background
72
+
73
+ window = Image.new("RGBA", (width, height), (0, 0, 0, 0))
74
+ draw = ImageDraw.Draw(window)
75
+
76
+ title_bar_height = 40
77
+ radius = 10
78
+ draw.rounded_rectangle([(0, 0), (width, height)], radius, fill=bg_color)
79
+
80
+ # Draw window control buttons
81
+ circle_y = title_bar_height // 2
82
+ draw.ellipse((20, circle_y - 6, 32, circle_y + 6), fill=(255, 95, 87))
83
+ draw.ellipse((40, circle_y - 6, 52, circle_y + 6), fill=(255, 189, 46))
84
+ draw.ellipse((60, circle_y - 6, 72, circle_y + 6), fill=(39, 201, 63))
85
+
86
+ if filename:
87
+ try:
88
+ font = ImageFont.truetype("Arial Bold", int(font_size * 0.5))
89
+ except:
90
+ font = ImageFont.load_default()
91
+ text_width = draw.textlength(filename, font=font)
92
+ text_x = (width - text_width) // 2
93
+ draw.text((text_x, circle_y - 6), filename, fill=(200, 200, 200), font=font)
94
+
95
+ return window
96
+
97
+
98
+ def load_and_resize_image(
99
+ image_path: str, target_width: int, window_padding: int
100
+ ) -> Tuple[Optional[Image.Image], int]:
101
+ """Load and resize an image from path or URL."""
102
+ try:
103
+ if urlparse(image_path).scheme in ("http", "https"):
104
+ response = requests.get(image_path)
105
+ img = Image.open(BytesIO(response.content))
106
+ else:
107
+ img = Image.open(image_path)
108
+
109
+ # Scale image to match target width
110
+ aspect = img.width / img.height
111
+ new_width = target_width - 2 * window_padding
112
+ new_height = int(new_width / aspect)
113
+ img = img.resize((new_width, new_height))
114
+
115
+ # Add padding
116
+ height_with_padding = img.height + 2 * window_padding
117
+ padded_img = Image.new(
118
+ "RGBA",
119
+ (img.width + 2 * window_padding, height_with_padding),
120
+ (0, 0, 0, 0),
121
+ )
122
+ padded_img.paste(img, (window_padding, window_padding))
123
+
124
+ return padded_img, height_with_padding
125
+ except Exception as e:
126
+ print(f"Warning: Could not load image: {e}")
127
+ return None, 0
128
+
129
+
130
+ def create_code_gif(
131
+ code: Union[str, Path],
132
+ output_file: str | None = None,
133
+ style: str = "monokai",
134
+ font_size: int = 24,
135
+ start_delay: float = 0.5,
136
+ end_delay: float = 1.0,
137
+ acceleration: float = 0.8,
138
+ line_numbers: bool = True,
139
+ gradient_start: Tuple[int, int, int] = (45, 49, 66),
140
+ gradient_end: Tuple[int, int, int] = (239, 129, 132),
141
+ title: Optional[str] = None,
142
+ filename: Optional[str] = None,
143
+ favicon: Optional[str] = None,
144
+ photo: Optional[str] = None,
145
+ photo_position: Literal["above", "below"] = "above",
146
+ comments: Optional[List[str]] = None,
147
+ comments_position: Literal["above", "below"] = "above",
148
+ aspect_ratio: float = 16 / 9,
149
+ ) -> None:
150
+ """Creates an animated GIF of code being typed out with increasing speed."""
151
+
152
+ # Initialize constants
153
+ min_padding = 20
154
+ window_padding = 20
155
+ title_bar_height = 40
156
+ title_font_size = int(font_size * 2.5)
157
+ comment_font_size = int(font_size * 2)
158
+ title_height = title_font_size * 2 if title else 0
159
+ comment_height = comment_font_size * 2 if comments else 0
160
+
161
+ # Load and process code
162
+ code_str = code.read_text() if isinstance(code, Path) else code
163
+
164
+ # Set up syntax highlighting
165
+ lexer = PythonLexer()
166
+ formatter = ImageFormatter(
167
+ style=style, line_numbers=line_numbers, font_size=font_size
168
+ )
169
+
170
+ # Get code dimensions
171
+ with tempfile.NamedTemporaryFile(suffix=".png") as tmp:
172
+ tmp.write(pygments.highlight(code_str, lexer, formatter))
173
+ tmp.flush()
174
+ with Image.open(tmp.name) as img:
175
+ code_width, code_height = img.size
176
+
177
+ total_width = code_width + 2 * window_padding
178
+ final_height = code_height + title_bar_height + 2 * window_padding
179
+
180
+ # Load photo if provided
181
+ photo_img, photo_height = (
182
+ load_and_resize_image(photo, total_width, window_padding)
183
+ if photo
184
+ else (None, 0)
185
+ )
186
+
187
+ # Calculate background dimensions
188
+ content_height = (
189
+ title_height + photo_height + comment_height + final_height + 4 * min_padding
190
+ )
191
+ background_width = max(
192
+ total_width + 2 * min_padding, int(content_height * aspect_ratio)
193
+ )
194
+ background_height = content_height
195
+ window_x = (background_width - total_width) // 2
196
+
197
+ # Load favicon
198
+ logo = None
199
+ if favicon:
200
+ logo_size = int(min(background_width, background_height) * 0.1)
201
+ try:
202
+ if favicon.lower().endswith(".svg"):
203
+ if urlparse(favicon).scheme in ("http", "https"):
204
+ response = requests.get(favicon)
205
+ png_data = cairosvg.svg2png(
206
+ bytestring=response.content,
207
+ output_width=logo_size,
208
+ output_height=logo_size,
209
+ )
210
+ else:
211
+ png_data = cairosvg.svg2png(
212
+ url=favicon,
213
+ output_width=logo_size,
214
+ output_height=logo_size,
215
+ )
216
+ logo = Image.open(BytesIO(png_data))
217
+ else:
218
+ logo, _ = load_and_resize_image(favicon, logo_size, 0)
219
+ if logo:
220
+ logo.thumbnail((logo_size, logo_size))
221
+ except Exception as e:
222
+ print(f"Warning: Could not load favicon: {e}")
223
+
224
+ # Pre-create fonts
225
+ try:
226
+ title_font = ImageFont.truetype("Arial Bold", title_font_size)
227
+ comment_font = ImageFont.truetype("Arial", comment_font_size)
228
+ except:
229
+ title_font = comment_font = ImageFont.load_default()
230
+
231
+ # Generate frames
232
+ frames = []
233
+ with tempfile.TemporaryDirectory() as tmpdir:
234
+ code_lines = code_str.split("\n")
235
+ num_frames = len(code_lines)
236
+ frames_per_comment = num_frames // len(comments) if comments else 0
237
+
238
+ # Pre-create window background
239
+ window = create_window_background(
240
+ total_width, final_height, style, filename, font_size
241
+ )
242
+
243
+ for i in range(num_frames):
244
+ current_code = "\n".join(code_lines[: i + 1])
245
+ highlighted = pygments.highlight(current_code, lexer, formatter)
246
+
247
+ temp_path = os.path.join(tmpdir, f"frame_{i}.png")
248
+ with open(temp_path, "wb") as f:
249
+ f.write(highlighted)
250
+
251
+ code_img = Image.open(temp_path)
252
+ background = create_gradient_background(
253
+ background_width, background_height, gradient_start, gradient_end, i
254
+ )
255
+
256
+ current_y = min_padding
257
+
258
+ # Add title
259
+ if title:
260
+ draw = ImageDraw.Draw(background)
261
+ text_width = draw.textlength(title, font=title_font)
262
+ text_x = (background_width - text_width) // 2
263
+ draw.text(
264
+ (text_x, current_y), title, fill=(255, 255, 255), font=title_font
265
+ )
266
+ current_y += title_height
267
+
268
+ # Add photo above
269
+ if photo_img and photo_position == "above":
270
+ photo_x = (background_width - photo_img.width) // 2
271
+ background.paste(photo_img, (photo_x, current_y), photo_img)
272
+ current_y += photo_height
273
+
274
+ # Add comments above
275
+ if comments and comments_position == "above":
276
+ draw = ImageDraw.Draw(background)
277
+ comment_idx = min(i // frames_per_comment, len(comments) - 1)
278
+ comment = comments[comment_idx]
279
+ text_width = draw.textlength(comment, font=comment_font)
280
+ text_x = (background_width - text_width) // 2
281
+ draw.text(
282
+ (text_x, current_y),
283
+ comment,
284
+ fill=(255, 255, 255),
285
+ font=comment_font,
286
+ )
287
+ current_y += comment_height
288
+
289
+ # Add code window
290
+ window_copy = window.copy()
291
+ window_copy.paste(
292
+ code_img, (window_padding, window_padding + title_bar_height)
293
+ )
294
+ background.paste(window_copy, (window_x, current_y), window_copy)
295
+ current_y += final_height
296
+
297
+ # Add photo below
298
+ if photo_img and photo_position == "below":
299
+ photo_x = (background_width - photo_img.width) // 2
300
+ current_y += min_padding
301
+ background.paste(photo_img, (photo_x, current_y), photo_img)
302
+ current_y += photo_height
303
+
304
+ # Add comments below
305
+ if comments and comments_position == "below":
306
+ draw = ImageDraw.Draw(background)
307
+ comment_idx = min(i // frames_per_comment, len(comments) - 1)
308
+ comment = comments[comment_idx]
309
+ text_width = draw.textlength(comment, font=comment_font)
310
+ text_x = (background_width - text_width) // 2
311
+ current_y += min_padding
312
+ draw.text(
313
+ (text_x, current_y),
314
+ comment,
315
+ fill=(255, 255, 255),
316
+ font=comment_font,
317
+ )
318
+
319
+ # Add logo
320
+ if logo:
321
+ logo_x = background_width - logo.width - min_padding
322
+ logo_y = background_height - logo.height - min_padding
323
+ background.paste(
324
+ logo, (logo_x, logo_y), logo if logo.mode == "RGBA" else None
325
+ )
326
+
327
+ frames.append(background)
328
+
329
+ # Calculate frame delays
330
+ delays = np.array(
331
+ [
332
+ start_delay * (acceleration ** (i / (len(frames) - 1)))
333
+ for i in range(len(frames))
334
+ ]
335
+ )
336
+ delays = np.clip(delays, end_delay, None)
337
+
338
+ if output_file is not None:
339
+ # Save the animated GIF
340
+ frames[0].save(
341
+ output_file,
342
+ save_all=True,
343
+ append_images=frames[1:],
344
+ duration=[int(d * 1000) for d in delays],
345
+ loop=0,
346
+ )
347
+ return frames