feat: add local image enhancement and lossless mod assets
This commit is contained in:
1 parent
cc71050362
commit
f7f7f5b1b3
29 files changed
+1495
-10
No files matched your search
+102
-1
@@ -1,5 +1,8 @@
|
||||
"""Mod editor, revision history, three-way merge and rebase."""
|
||||
import base64
|
||||
import binascii
|
||||
import copy
|
||||
import math
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal
|
||||
@@ -81,11 +84,16 @@ async def save_editor(version_id: int, body: EditIn, user: Editor, db: DB):
|
||||
|
||||
@router.put("/{version_id}/asset")
|
||||
async def replace_asset(version_id: int, path: str, revision: str, filename: str,
|
||||
request: Request, user: Editor, db: DB):
|
||||
request: Request, user: Editor, db: DB, lossless: bool = False,
|
||||
scale: float | None = None):
|
||||
await get_version(db, user, version_id)
|
||||
v = await lock_version(db, version_id)
|
||||
editable(v, user)
|
||||
expected(v, revision)
|
||||
if scale is not None and (not math.isfinite(scale) or not 0 < scale <= 1024 or assets.kind_of(Path(path)) != "image"):
|
||||
raise HTTPException(422, "Некорректный масштаб изображения")
|
||||
if lossless and (assets.kind_of(Path(path)) != "image" or Path(filename).suffix.lower() not in (".png", ".webp")):
|
||||
raise HTTPException(422, "Замена без потерь принимает только PNG/WebP")
|
||||
if path not in v.manifest["files"] or path == "game.json" or path.startswith(("script/", "locales/")):
|
||||
raise HTTPException(422, "Выберите существующий медиафайл")
|
||||
suffix = Path(filename).suffix.lower()
|
||||
@@ -105,6 +113,9 @@ async def replace_asset(version_id: int, path: str, revision: str, filename: str
|
||||
if not count:
|
||||
raise HTTPException(422, "Пустой файл")
|
||||
def convert():
|
||||
if lossless:
|
||||
output, ext = assets.lossless_image(source, root / "lossless")
|
||||
return put_file(output, ext, move=True)
|
||||
# Do not accept unparsed webm passthrough for editor uploads.
|
||||
if suffix == ".webm":
|
||||
output = assets._ffmpeg(source, root / "verified.webm",
|
||||
@@ -123,6 +134,96 @@ async def replace_asset(version_id: int, path: str, revision: str, filename: str
|
||||
raise HTTPException(422, f"Не удалось обработать файл: {e}") from None
|
||||
manifest = copy.deepcopy(v.manifest)
|
||||
manifest["files"][path] = {"hash": blob_id, "size": size}
|
||||
await run_in_threadpool(set_asset_scale, manifest, {path: scale})
|
||||
update_manifest(v, manifest)
|
||||
await db.commit()
|
||||
return {"revision": v.revision_id, "files": v.manifest["files"]}
|
||||
|
||||
|
||||
def set_asset_scale(manifest, scales):
|
||||
game = copy.deepcopy(file_json(manifest, "game.json"))
|
||||
values = game.setdefault("assetScale", {})
|
||||
for path, scale in scales.items():
|
||||
if scale is None or scale == 1:
|
||||
values.pop(path, None)
|
||||
else:
|
||||
values[path] = scale
|
||||
put_json(manifest["files"], "game.json", game)
|
||||
|
||||
|
||||
class RestoreAssetIn(BaseModel):
|
||||
revision: str
|
||||
path: str
|
||||
|
||||
|
||||
@router.post("/{version_id}/asset/restore")
|
||||
async def restore_asset(version_id: int, body: RestoreAssetIn, user: Editor, db: DB):
|
||||
await get_version(db, user, version_id)
|
||||
v = await lock_version(db, version_id)
|
||||
editable(v, user)
|
||||
expected(v, body.revision)
|
||||
base = await db.get(Revision, v.base_revision_id) if v.base_revision_id else None
|
||||
if not base or base.game_id != v.game_id or body.path not in base.manifest["files"] or assets.kind_of(Path(body.path)) != "image":
|
||||
raise HTTPException(422, "В базовой ревизии нет этого изображения")
|
||||
manifest = copy.deepcopy(v.manifest)
|
||||
manifest["files"][body.path] = copy.deepcopy(base.manifest["files"][body.path])
|
||||
scale = file_json(base.manifest, "game.json").get("assetScale", {}).get(body.path)
|
||||
await run_in_threadpool(set_asset_scale, manifest, {body.path: scale})
|
||||
update_manifest(v, manifest)
|
||||
await db.commit()
|
||||
return {"revision": v.revision_id, "files": v.manifest["files"]}
|
||||
|
||||
|
||||
class AssetIn(BaseModel):
|
||||
path: str
|
||||
filename: str
|
||||
data: str # base64 PNG/WebP, without the data URL prefix
|
||||
scale: float = Field(gt=0, le=1024, allow_inf_nan=False)
|
||||
|
||||
|
||||
class AssetsIn(BaseModel):
|
||||
revision: str
|
||||
assets: list[AssetIn] = Field(min_length=1, max_length=32)
|
||||
|
||||
|
||||
@router.post("/{version_id}/assets")
|
||||
async def replace_assets(version_id: int, request: Request, user: Editor, db: DB):
|
||||
"""One atomic image-only replacement and revision for a bounded batch."""
|
||||
await get_version(db, user, version_id)
|
||||
v = await lock_version(db, version_id)
|
||||
editable(v, user)
|
||||
raw = bytearray()
|
||||
async for chunk in request.stream():
|
||||
raw.extend(chunk)
|
||||
if len(raw) > 128 * 1024 * 1024:
|
||||
raise HTTPException(413, "Максимум 128 МБ на пакет")
|
||||
try:
|
||||
body = AssetsIn.model_validate_json(raw)
|
||||
except ValueError:
|
||||
raise HTTPException(422, "Некорректный пакет изображений") from None
|
||||
expected(v, body.revision)
|
||||
if len({a.path for a in body.assets}) != len(body.assets):
|
||||
raise HTTPException(422, "Повторяющиеся пути в пакете")
|
||||
for a in body.assets:
|
||||
if a.path not in v.manifest["files"] or assets.kind_of(Path(a.path)) != "image" or Path(a.filename).suffix.lower() not in (".png", ".webp"):
|
||||
raise HTTPException(422, "Выберите существующее изображение и PNG/WebP")
|
||||
def build():
|
||||
manifest = copy.deepcopy(v.manifest)
|
||||
get_settings().work_dir.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(dir=get_settings().work_dir) as folder:
|
||||
root = Path(folder)
|
||||
for i, item in enumerate(body.assets):
|
||||
source = root / f"source{i}{Path(item.filename).suffix.lower()}"
|
||||
source.write_bytes(base64.b64decode(item.data, validate=True))
|
||||
output, ext = assets.lossless_image(source, root / f"out{i}")
|
||||
blob_id, size = put_file(output, ext, move=True)
|
||||
manifest["files"][item.path] = {"hash": blob_id, "size": size}
|
||||
set_asset_scale(manifest, {a.path: a.scale for a in body.assets})
|
||||
return manifest
|
||||
try:
|
||||
manifest = await run_in_threadpool(build)
|
||||
except (ValueError, OSError, binascii.Error) as e:
|
||||
raise HTTPException(422, f"Не удалось обработать пакет: {e}") from None
|
||||
update_manifest(v, manifest)
|
||||
await db.commit()
|
||||
return {"revision": v.revision_id, "files": v.manifest["files"]}
|
||||
|
||||
Reference in new issue
Block a user