UniversalTool/app/backend/converter.py

188 lines
5.6 KiB
Python
Raw Normal View History

2026-07-07 18:19:47 +05:00
import subprocess
from pathlib import Path
from PIL import Image
from backend.downloader import ffmpeg_path
IMAGE_FORMATS = {"jpg", "jpeg", "png", "webp", "bmp", "gif", "tiff", "ico"}
AUDIO_FORMATS = {"mp3", "flac", "wav", "m4a", "ogg", "opus", "aac"}
VIDEO_FORMATS = {"mp4", "avi", "mov", "mkv", "webm", "gif"}
DOC_FORMATS = {"pdf", "pptx"}
ALL_TARGETS = IMAGE_FORMATS | AUDIO_FORMATS | VIDEO_FORMATS | DOC_FORMATS
ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]
def detect_type(path: Path) -> str:
ext = path.suffix.lstrip(".").lower()
if ext in IMAGE_FORMATS:
return "image"
if ext in AUDIO_FORMATS:
return "audio"
if ext in VIDEO_FORMATS or ext in {"flv", "wmv", "m4v", "3gp"}:
return "video"
if ext == "pdf":
return "pdf"
if ext == "pptx":
return "pptx"
return "unknown"
def get_targets(kind: str) -> list:
if kind == "image":
return sorted(IMAGE_FORMATS | {"pdf", "pptx"})
if kind == "audio":
return sorted(AUDIO_FORMATS)
if kind == "video":
return sorted(VIDEO_FORMATS | AUDIO_FORMATS)
if kind == "pdf":
return sorted(IMAGE_FORMATS | {"pptx"})
if kind == "pptx":
return ["pdf"] + sorted(IMAGE_FORMATS - {"ico"})
return []
def _convert_image(src: Path, dst: Path, target: str):
img = Image.open(src)
if target == "ico":
rgba = img.convert("RGBA")
sizes = [(s, s) for s in ICO_SIZES if s <= max(rgba.size)] or [(32, 32)]
rgba.save(dst, format="ICO", sizes=sizes)
return
if target in ("jpg", "jpeg"):
img = img.convert("RGB")
fmt = "JPEG" if target in ("jpg", "jpeg") else target.upper()
img.save(dst, format=fmt)
def _convert_image_to_pdf(src: Path, dst: Path):
import img2pdf
with open(dst, "wb") as f:
f.write(img2pdf.convert(str(src)))
EMU_PER_PX = 9525
EMU_MIN = 914400 # 1 inch
EMU_MAX = 51206400 # 56 inches
def _slide_dims(w_px: int, h_px: int) -> tuple[int, int]:
w = w_px * EMU_PER_PX
h = h_px * EMU_PER_PX
smallest = min(w, h)
largest = max(w, h)
if smallest < EMU_MIN:
scale = EMU_MIN / smallest
w, h = int(w * scale), int(h * scale)
elif largest > EMU_MAX:
scale = EMU_MAX / largest
w, h = int(w * scale), int(h * scale)
return w, h
def _convert_image_to_pptx(src: Path, dst: Path):
from pptx import Presentation
from pptx.util import Emu
img = Image.open(src)
w_px, h_px = img.size
prs = Presentation()
prs.slide_width, prs.slide_height = _slide_dims(w_px, h_px)
slide = prs.slides.add_slide(prs.slide_layouts[6])
slide.shapes.add_picture(str(src), 0, 0, width=prs.slide_width, height=prs.slide_height)
prs.save(dst)
def _convert_pdf_to_images_or_pptx(src: Path, dst: Path, target: str):
import fitz # pymupdf
doc = fitz.open(src)
if target == "pptx":
from pptx import Presentation
import tempfile
prs = Presentation()
with tempfile.TemporaryDirectory() as tmp:
for i, page in enumerate(doc):
pix = page.get_pixmap(dpi=150)
page_img = Path(tmp) / f"page_{i}.png"
pix.save(page_img)
if i == 0:
prs.slide_width, prs.slide_height = _slide_dims(pix.width, pix.height)
slide = prs.slides.add_slide(prs.slide_layouts[6])
slide.shapes.add_picture(str(page_img), 0, 0, width=prs.slide_width, height=prs.slide_height)
prs.save(dst)
return
# single page -> single image; only first page is used for image targets
page = doc[0]
pix = page.get_pixmap(dpi=200)
pix.save(dst)
def _convert_pptx_to_pdf(src: Path, dst: Path):
raise RuntimeError(
"PPTX -> PDF/картинка требует LibreOffice или PowerPoint (рендер слайдов) — "
"не поддерживается без тяжёлых внешних зависимостей."
)
def _convert_audio_video(src: Path, dst: Path, target: str):
ffmpeg_dir = ffmpeg_path()
exe = str(Path(ffmpeg_dir) / "ffmpeg.exe") if ffmpeg_dir else "ffmpeg"
args = [exe, "-y", "-i", str(src)]
if target == "gif":
args += ["-vf", "fps=12,scale=480:-1:flags=lanczos"]
elif target in AUDIO_FORMATS:
args += ["-vn"]
if target == "mp3":
args += ["-codec:a", "libmp3lame", "-b:a", "320k"]
elif target == "flac":
args += ["-codec:a", "flac"]
elif target == "wav":
args += ["-codec:a", "pcm_s16le"]
elif target == "aac":
args += ["-codec:a", "aac", "-b:a", "256k"]
args.append(str(dst))
result = subprocess.run(args, capture_output=True, text=True, timeout=600)
if result.returncode != 0:
raise RuntimeError(f"ffmpeg error: {result.stderr[-800:]}")
def convert(src: Path, dst: Path, target: str) -> tuple[bool, str | None]:
try:
kind = detect_type(src)
if kind == "image":
if target == "pdf":
_convert_image_to_pdf(src, dst)
elif target == "pptx":
_convert_image_to_pptx(src, dst)
else:
_convert_image(src, dst, target)
elif kind == "pdf":
_convert_pdf_to_images_or_pptx(src, dst, target)
elif kind == "pptx":
_convert_pptx_to_pdf(src, dst)
elif kind in ("audio", "video"):
_convert_audio_video(src, dst, target)
else:
return False, f"неизвестный формат: {src.suffix}"
return True, None
except Exception as e:
return False, str(e)