import math import os import re import tempfile from flask import Flask, render_template, request, send_file, Response from PIL import Image, ImageOps, ImageDraw, ImageColor import numpy as np # Проверка наличия MoviePy try: from moviepy.editor import VideoFileClip MOVIEPY_AVAILABLE = True except ImportError: MOVIEPY_AVAILABLE = False app = Flask(__name__) # --- Вспомогательные функции --- def prepare_svg_content(file_storage): """Читает SVG и готовит его для вставки""" try: raw_data = file_storage.read().decode('utf-8') vb_match = re.search(r'viewBox=["\']([^"\']+)["\']', raw_data, re.I) vbox = vb_match.group(1) if vb_match else "0 0 100 100" inner = re.sub(r']*>', '', raw_data, flags=re.I) inner = re.sub(r'', '', inner, flags=re.I) inner = re.sub(r'<\?xml[^>]*\?>|]*>', '', inner, flags=re.I) return inner, vbox except Exception: return '', "0 0 100 100" def calculate_grid_items(width, height, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_data=None): """Математическое ядро расчета сетки""" step = width / density rows = math.ceil(height / step) cell_w_max = step * (1.0 - gap_x) cell_h_max = step * (1.0 - gap_y) items = [] start_c = -int(density * 0.5) end_c = int(density * 1.5) for r in range(rows): row_shift = (r * shift * step) for c in range(start_c, end_c): grid_x = c * step + row_shift grid_y = r * step if grid_x < -step or grid_x > width + step: continue # Логика яркости is_visible = True if pixel_data is not None: sx = max(0, min(int(grid_x + step / 2), width - 1)) sy = max(0, min(int(grid_y + step / 2), height - 1)) try: val = pixel_data.getpixel((sx, sy)) brightness = val / 255.0 limit = 0.5 is_visible = (brightness > limit) if invert else (brightness < limit) except Exception: pass if not is_visible: continue # Логика искажения (Growth) wave = 0 if growth_type == 'sine': wave = (math.sin(c * 0.3 + r * 0.3) + 1) / 2 elif growth_type == 'linear': wave = (c % density) / density elif growth_type == 'steps': wave = (r % 5) / 5.0 mod_w = 1.0 - (wave * growth) mod_w = max(0.01, min(1.0, mod_w)) content_w = cell_w_max * base_scale * mod_w content_h = cell_h_max * base_scale cx = grid_x + step / 2 cy = grid_y + step / 2 items.append({ 'cx': cx, 'cy': cy, 'w': content_w, 'h': content_h }) return items, step # --- Роуты --- @app.route('/') def index(): return render_template('index.html') @app.route('/download_svg', methods=['POST']) def download_svg(): try: # Извлечение параметров density = int(request.form.get('density', 24)) gap_x = float(request.form.get('gap_x', 0.2)) gap_y = float(request.form.get('gap_y', 0.0)) shift = float(request.form.get('shift_stagger', 0.34)) base_scale = float(request.form.get('base_scale', 1.0)) growth = float(request.form.get('growth', 0.9)) growth_type = request.form.get('growth_type', 'sine') invert = request.form.get('invert', 'false').lower() == 'true' bg_color = request.form.get('bg_color', '#ffffff') fill_color = request.form.get('fill_color', '#000000') canvas_w, canvas_h = 1400, 900 # SVG Symbol по умолчанию или из файла symbol_content = '' v_box = "0 0 100 100" svg_file = request.files.get('shape_svg') if svg_file and svg_file.filename != '': symbol_content, v_box = prepare_svg_content(svg_file) vb_parts = [float(x) for x in v_box.split()] vb_w, vb_h = vb_parts[2], vb_parts[3] # Raster Image для маски raster_file = request.files.get('raster_image') pixel_img = None if raster_file and raster_file.filename != '': img = Image.open(raster_file) img = ImageOps.fit(img, (canvas_w, canvas_h), method=Image.Resampling.LANCZOS) pixel_img = ImageOps.grayscale(img) items, _ = calculate_grid_items( canvas_w, canvas_h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, pixel_img ) body = [] for item in items: sx = item['w'] / vb_w sy = item['h'] / vb_h transform = f'translate({item["cx"]:.2f} {item["cy"]:.2f}) scale({sx:.4f} {sy:.4f}) translate({-vb_w/2:.2f} {-vb_h/2:.2f})' body.append(f'{symbol_content}') svg_str = f'{"".join(body)}' return Response( svg_str, mimetype="image/svg+xml", headers={"Content-disposition": "attachment; filename=pattern.svg"} ) except Exception as e: return str(e), 500 @app.route('/process_video', methods=['POST']) def process_video(): if not MOVIEPY_AVAILABLE: return "MoviePy not installed on server", 500 temp_dir = tempfile.mkdtemp() try: density = int(request.form.get('density', 24)) gap_x = float(request.form.get('gap_x', 0.2)) gap_y = float(request.form.get('gap_y', 0.0)) shift = float(request.form.get('shift_stagger', 0.34)) base_scale = float(request.form.get('base_scale', 1.0)) growth = float(request.form.get('growth', 0.9)) growth_type = request.form.get('growth_type', 'sine') invert = request.form.get('invert', 'false').lower() == 'true' bg_color = request.form.get('bg_color', '#ffffff') fill_color = request.form.get('fill_color', '#000000') video_file = request.files.get('video_file') if not video_file: return "No video uploaded", 400 input_path = os.path.join(temp_dir, 'input.mp4') video_file.save(input_path) def process_frame(get_frame, t): frame = get_frame(t) img_pil = Image.fromarray(frame).convert("L") w, h = img_pil.size canvas_img = Image.new("RGB", (w, h), bg_color) draw = ImageDraw.Draw(canvas_img) items, _ = calculate_grid_items( w, h, density, gap_x, gap_y, shift, base_scale, growth, growth_type, invert, img_pil ) for item in items: x0 = item['cx'] - item['w'] / 2 y0 = item['cy'] - item['h'] / 2 draw.rectangle([x0, y0, x0 + item['w'], y0 + item['h']], fill=fill_color) return np.array(canvas_img) clip = VideoFileClip(input_path) new_clip = clip.fl(process_frame) output_path = os.path.join(temp_dir, 'output.mp4') new_clip.write_videofile(output_path, codec='libx264', audio_codec='aac', fps=clip.fps) return send_file(output_path, as_attachment=True, download_name="processed_video.mp4") except Exception as e: return f"Error processing video: {str(e)}", 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=7860)