Add advanced canvas editing and Ubuntu deployment

This commit is contained in:
2026-07-14 20:32:42 +08:00
parent f8a907e7c5
commit bf2b138007
19 changed files with 2026 additions and 255 deletions
+68 -17
View File
@@ -885,27 +885,76 @@ async def upload_asset(
return Asset(**meta)
@app.post("/api/assets/from-job/{job_id}", response_model=Asset)
async def import_asset_from_job(
job_id: str,
name: str = Form(""),
) -> Asset:
# 尝试从内存获取产物路径;若服务已重启则从磁盘回退
def _svg_with_transparent_background(svg_bytes: bytes) -> bytes:
"""Convert full-canvas opaque backgrounds to transparent for sticker use.
Job pipeline SVG (`to_svg`) embeds a white full-rect so standalone preview
looks solid. Canvas stickers must stay transparent so the mask layer shows
through. Only the full-canvas background rect is touched.
"""
try:
text = svg_bytes.decode("utf-8")
except UnicodeDecodeError:
text = svg_bytes.decode("utf-8", errors="ignore")
patterns = [
# <rect width="100%" height="100%" fill="white"/>
(
r'(<rect\b[^>]*\bwidth=["\']100%["\'][^>]*\bheight=["\']100%["\'][^>]*\bfill=["\'])(?:white|#fff(?:fff)?|rgb\(\s*255\s*,\s*255\s*,\s*255\s*\))(["\'][^>]*?/?>)',
r'\1none\2',
),
# attribute order: fill before width/height
(
r'(<rect\b[^>]*\bfill=["\'])(?:white|#fff(?:fff)?|rgb\(\s*255\s*,\s*255\s*,\s*255\s*\))(["\'][^>]*\bwidth=["\']100%["\'][^>]*\bheight=["\']100%["\'][^>]*?/?>)',
r'\1none\2',
),
]
for pattern, repl in patterns:
updated, n = re.subn(pattern, repl, text, count=1, flags=re.IGNORECASE)
if n:
return updated.encode("utf-8")
return svg_bytes.encode("utf-8") if isinstance(svg_bytes, str) else svg_bytes
def _find_job_svg(job_id: str) -> Path | None:
"""Locate the main (non-stroke) SVG for a job.
Prefer in-memory artifact path when valid; always fall back to the job
workspace on disk so empty/stale memory state cannot hide existing files.
"""
svg_path: Path | None = None
if manager.exists(job_id):
status = manager.get_status(job_id)
svg_path_str = status.artifacts.get("svg", "")
if svg_path_str:
svg_path = Path(svg_path_str)
else:
# 服务重启后内存丢失,直接从工作区目录查找
fallback_dir = storage.base_dir / job_id / "output"
if fallback_dir.exists():
for candidate in fallback_dir.glob("*.svg"):
candidate = Path(svg_path_str)
if candidate.exists():
svg_path = candidate
break
if svg_path is not None:
return svg_path
fallback_dir = storage.base_dir / job_id / "output"
if not fallback_dir.exists():
return None
candidates = sorted(fallback_dir.glob("*.svg"))
for candidate in candidates:
if candidate.name.endswith("_stroke.svg"):
continue
return candidate
return candidates[0] if candidates else None
@app.post("/api/assets/from-job/{job_id}", response_model=Asset)
async def import_asset_from_job(
job_id: str,
name: str = Form(""),
type: str = Form("wordcloud"),
) -> Asset:
svg_path = _find_job_svg(job_id)
if svg_path is None or not svg_path.exists():
raise HTTPException(status_code=404, detail="svg not found")
@@ -914,16 +963,18 @@ async def import_asset_from_job(
asset_path.mkdir(parents=True, exist_ok=True)
dest = asset_path / "asset.svg"
shutil.copy2(svg_path, dest)
content = dest.read_bytes()
raw = svg_path.read_bytes()
# 画布贴纸需要透明底;任务主 SVG 默认带白底 rect
content = _svg_with_transparent_background(raw)
dest.write_bytes(content)
width, height = _parse_svg_viewbox(dest)
asset_type = (type or "wordcloud").strip() or "wordcloud"
display_name = name or f"词云_{job_id[:8]}"
meta = {
"asset_id": asset_id,
"name": display_name,
"type": "wordcloud",
"type": asset_type,
"mime_type": "image/svg+xml",
"width": width,
"height": height,