diff --git a/README.md b/README.md index 16432ee..7b4c679 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PicGen Console -一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.62**。它把 +一个面向 OpenAI 兼容图像生成 / 编辑接口的本地工作台,当前版本 **0.1.63**。它把 `/v1/images/generations`、`/v1/images/edits` 与 `/v1/responses`(含 `image_generation` 工具) 包装成统一可观测的代理,前端是一套零依赖的 Web 控制台。 @@ -14,7 +14,9 @@  -## 0.1.62 主要特性 +## 0.1.63 主要特性 + +- **行程图按需嵌入标题字体**:仅打包本次标题和日期实际使用的字形,保留原字体效果,同时显著降低 SVG 体积和客户浏览器解码压力。 - **简洁模式可直接接收分享**:主页新增“收到分享”区块,支持刷新并直接打开同事分享的成品;新用户无需先切换到专业模式。 @@ -123,10 +125,10 @@ PICGEN_LOG_FORMAT=json \ ### Docker ```bash -docker build -t minorli/picgen:0.1.62 . +docker build -t minorli/picgen:0.1.63 . docker run --rm -p 8000:8000 \ -v picgen-data:/app/data \ - minorli/picgen:0.1.62 + minorli/picgen:0.1.63 ``` 或: @@ -141,10 +143,10 @@ docker compose up -d ./scripts/docker-build-push.sh ``` -默认会构建并推送 `minorli/picgen:0.1.62`。也可以覆盖: +默认会构建并推送 `minorli/picgen:0.1.63`。也可以覆盖: ```bash -IMAGE=minorli/picgen VERSION=0.1.62 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh +IMAGE=minorli/picgen VERSION=0.1.63 PLATFORM=linux/amd64 ./scripts/docker-build-push.sh ``` 镜像不会包含 `.env`、本地用户库或历史图片。容器内置 `HEALTHCHECK` 探测 `/api/health`,以非 root @@ -257,7 +259,7 @@ Bug 反馈和找回密码申请会先写入本地认证库,再优先发送到 ## 图像通道 -PicGen 0.1.62 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定: +PicGen 0.1.63 把四类图像操作统一提交给 `/api/image-jobs`,实际通道由服务端决定: | 用户操作 | 默认接口 | 默认模型 | | --- | --- | --- | diff --git a/docker-compose.yml b/docker-compose.yml index 55989e7..0428773 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: picgen: - image: minorli/picgen:0.1.62 + image: minorli/picgen:0.1.63 build: context: . ports: diff --git a/pyproject.toml b/pyproject.toml index 756aa19..07b29e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "picgen" -version = "0.1.62" +version = "0.1.63" description = "Enterprise-grade local web console for OpenAI-compatible image generation and editing APIs." readme = "README.md" requires-python = ">=3.12" @@ -23,6 +23,7 @@ dependencies = [ "pydantic-settings>=2.4.0", "anyio>=4.4.0", "pillow>=11.0.0", + "fonttools>=4.0.0", ] [project.optional-dependencies] diff --git a/scripts/docker-build-push.sh b/scripts/docker-build-push.sh index c9a86cd..6ab3b0d 100755 --- a/scripts/docker-build-push.sh +++ b/scripts/docker-build-push.sh @@ -2,7 +2,7 @@ set -euo pipefail IMAGE="${IMAGE:-minorli/picgen}" -VERSION="${VERSION:-0.1.62}" +VERSION="${VERSION:-0.1.63}" PLATFORM="${PLATFORM:-linux/amd64}" docker buildx build \ diff --git a/src/picgen/itinerary_map.py b/src/picgen/itinerary_map.py index 77fb3e2..2972987 100644 --- a/src/picgen/itinerary_map.py +++ b/src/picgen/itinerary_map.py @@ -3,6 +3,7 @@ import asyncio import base64 import html +import logging import math import os import re @@ -14,11 +15,14 @@ from collections.abc import Awaitable, Callable from datetime import datetime from functools import lru_cache +from io import BytesIO from pathlib import Path from typing import Any from urllib import parse import httpx +from fontTools import subset as font_subset +from fontTools.ttLib import TTFont from .config import Settings from .restricted_destinations import stop_has_restricted_destination @@ -32,6 +36,8 @@ LOGO_HREF = "6renyou.png" TITLE_FONT_FAMILY = "PicGenRouteTitle" TITLE_FONT_RELATIVE_PATH = Path("fonts/zcool-xiaowei/ZCOOLXiaoWei-Regular.ttf") +logger = logging.getLogger(__name__) +logging.getLogger("fontTools.subset").setLevel(logging.WARNING) INSTRUCTION_STOP_PREFIXES = ( "地点与地理校验", "地理校验", @@ -81,14 +87,29 @@ def _candidate_static_dirs() -> list[Path]: return deduped -@lru_cache(maxsize=1) -def _embedded_title_font_face_css_cached() -> str: +def _subset_title_font_bytes(font_path: Path, glyphs: str) -> bytes: + font = TTFont(font_path, recalcTimestamp=False) + try: + subsetter = font_subset.Subsetter(options=font_subset.Options()) + subsetter.populate(text=glyphs) + subsetter.subset(font) + output = BytesIO() + font.save(output, reorderTables=False) + return output.getvalue() + finally: + font.close() + + +@lru_cache(maxsize=64) +def _embedded_title_font_face_css_cached(glyphs: str) -> str: for static_dir in _candidate_static_dirs(): - font_path = static_dir / TITLE_FONT_RELATIVE_PATH try: - font_bytes = font_path.read_bytes() + font_bytes = _subset_title_font_bytes(static_dir / TITLE_FONT_RELATIVE_PATH, glyphs) except OSError: continue + except Exception as exc: + logger.warning("itinerary_title_font_subset_failed", extra={"error_type": type(exc).__name__}) + return "" encoded = base64.b64encode(font_bytes).decode("ascii") return ( "/* ZCOOL XiaoWei, SIL Open Font License 1.1 */" @@ -99,11 +120,14 @@ def _embedded_title_font_face_css_cached() -> str: return "" -def _embedded_title_font_face_css() -> str: +def _embedded_title_font_face_css(text: str) -> str: # Only cache SUCCESS: a transient read failure (missing volume, bad cwd at # first render) must not permanently strip the title font from every # poster this process renders afterwards. - css = _embedded_title_font_face_css_cached() + glyphs = "".join(sorted(set(text))) + if not glyphs: + return "" + css = _embedded_title_font_face_css_cached(glyphs) if not css: _embedded_title_font_face_css_cached.cache_clear() return css @@ -1743,8 +1767,10 @@ def render_itinerary_map_svg( f'' for segment in route_segments ] - title = _svg_text(plan.get("title") or "定制旅行路线图") - subtitle = _svg_text(plan.get("subtitle") or "") + title_text = str(plan.get("title") or "定制旅行路线图") + subtitle_text = str(plan.get("subtitle") or "") + title = _svg_text(title_text) + subtitle = _svg_text(subtitle_text) safe_background_url = ( background_image_url if background_image_url.startswith( @@ -1836,7 +1862,7 @@ def render_itinerary_map_svg( f'' ) - title_font_face_css = _embedded_title_font_face_css() if safe_background_url else "" + title_font_face_css = _embedded_title_font_face_css(f"{title_text}{subtitle_text}") if safe_background_url else "" return "\n".join( [ svg_open[:-1] + f' data-density="{"dense" if dense_layout else "normal"}">', diff --git a/static/app.js b/static/app.js index f4eff6a..c157e2f 100644 --- a/static/app.js +++ b/static/app.js @@ -2,14 +2,14 @@ import { calculateLogoPlacementScore, calculateOfficialLogoPixelMatch, chooseLogoPlacement, -} from "./logo-placement.mjs?v=0.1.62" +} from "./logo-placement.mjs?v=0.1.63" import { DEFAULT_RESPONSES_MODEL, RESPONSES_MODEL_STORAGE_VERSION, RESPONSES_REASONING_STORAGE_VERSION, migrateStoredResponsesReasoningSettings, migrateStoredResponsesSettings, -} from "./responses-settings.mjs?v=0.1.62" +} from "./responses-settings.mjs?v=0.1.63" const RESPONSES_REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max", "ultra"]) const DEFAULT_RESPONSES_REASONING_EFFORT = "xhigh" diff --git a/static/index.html b/static/index.html index 4a0eef7..c7cbaf7 100644 --- a/static/index.html +++ b/static/index.html @@ -5,7 +5,7 @@ PicGen Console - + @@ -1061,7 +1061,7 @@ 管理员高级设置 @@ -1371,6 +1371,6 @@ 图片预览 - +