188 lines
5.6 KiB
Python
188 lines
5.6 KiB
Python
|
|
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)
|