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'', '', 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''
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)