新增配图平铺水印功能
parent
81a4173155
commit
846715d173
|
|
@ -13,7 +13,7 @@
|
||||||
- 开户教程、保号教程、转账 / 入金 / 出金教程
|
- 开户教程、保号教程、转账 / 入金 / 出金教程
|
||||||
- 美股券商、数字货币、欧元通道、汇款路径
|
- 美股券商、数字货币、欧元通道、汇款路径
|
||||||
- 避坑经验、实测记录、活动规则整理
|
- 避坑经验、实测记录、活动规则整理
|
||||||
- MAOMOMO 风格封面图、截图槽位、信息图和小红书卡片
|
- MAOMOMO 风格封面图、截图槽位、信息图、小红书卡片和配图平铺水印
|
||||||
|
|
||||||
不适合直接生成投资建议、法律意见、税务结论或任何“保证获批 / 稳赚 / 一定到账”的承诺型内容。
|
不适合直接生成投资建议、法律意见、税务结论或任何“保证获批 / 稳赚 / 一定到账”的承诺型内容。
|
||||||
|
|
||||||
|
|
@ -141,6 +141,7 @@ GitHub 交付配置:
|
||||||
- 多张 3:4 或 9:16 独立 PNG
|
- 多张 3:4 或 9:16 独立 PNG
|
||||||
- `prompts/`、`slide_jobs.json`、`slide_run_state.json`
|
- `prompts/`、`slide_jobs.json`、`slide_run_state.json`
|
||||||
- `final_manifest.json`
|
- `final_manifest.json`
|
||||||
|
- 可选 `watermarked_assets/`:用 `uv run python scripts/maomomo_tile_watermark.py` 生成 `MAOMOMO.COM` 45° 白色半透明平铺水印版
|
||||||
- GitHub 分批链接
|
- GitHub 分批链接
|
||||||
|
|
||||||
GitHub 分批交付前,技能会先引导配置:
|
GitHub 分批交付前,技能会先引导配置:
|
||||||
|
|
@ -187,7 +188,7 @@ GitHub 分批交付前,技能会先引导配置:
|
||||||
- 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞
|
- 涉及金融、券商、银行、数字货币和合规内容时,保留谨慎措辞
|
||||||
- 配图统一使用白橘猫、暖橙色、干净金融科技教程风
|
- 配图统一使用白橘猫、暖橙色、干净金融科技教程风
|
||||||
- 如果用户没有指定配图风格,先在 `recommended-styles.md` 和 `image-styles/` 中按文章类型推荐 2-3 个方向,例如 `clean-professional`、`data-card-dashboard`、`handdrawn-note`、`e-ink-editorial` 或 `xiaohongshu-vertical`。
|
- 如果用户没有指定配图风格,先在 `recommended-styles.md` 和 `image-styles/` 中按文章类型推荐 2-3 个方向,例如 `clean-professional`、`data-card-dashboard`、`handdrawn-note`、`e-ink-editorial` 或 `xiaohongshu-vertical`。
|
||||||
- 小红书卡片出图前必须确认 `2D 扁平风` / `3D 轻拟物风` 和 `3:4` / `9:16`;用户没指定时默认推荐 2D,并按原生竖版重新构图,不能把横版图压进竖图。
|
- 小红书卡片出图前必须确认 `2D 扁平风` / `3D 轻拟物风` 和 `3:4` / `9:16`;用户没指定时默认推荐 2D,并按原生竖版重新构图,不能把横版图压进竖图。用户要求防盗图 / 水印时,可用 `uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets` 生成 `MAOMOMO.COM` 45° 白色半透明平铺水印版,并保留无水印原图。
|
||||||
|
|
||||||
文章不应当:
|
文章不应当:
|
||||||
|
|
||||||
|
|
|
||||||
7
SKILL.md
7
SKILL.md
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: maomomo-article-writer
|
name: maomomo-article-writer
|
||||||
description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融实操文章、发布用 Markdown、SEO 稿、配图规划、独立高清配图 assets、ZIP 发布包,以及小红书卡片 / 多图 / 3:4 / 9:16 竖版 PNG 交付;包含 GitHub 交付仓库和专用 KEY / token / deploy key 配置引导。适用于 maomomo.com、港卡、香港银行、香港信用卡、美股券商、数字货币、汇款、支付优化、返现活动、开户教程、保号教程、避坑经验、官方条款整理和实测攻略。用户要求使用 skill、按猫MOMO风格、完整文章、发布用 Markdown、配图、打包、一键下载、最终交付、小红书、卡片、多图、3:4、9:16、GitHub 交付、仓库配置或专用 KEY 配置时触发。
|
description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融实操文章、发布用 Markdown、SEO 稿、配图规划、独立高清配图 assets、ZIP 发布包,以及小红书卡片 / 多图 / 3:4 / 9:16 竖版 PNG 交付、配图平铺水印;包含 GitHub 交付仓库和专用 KEY / token / deploy key 配置引导。适用于 maomomo.com、港卡、香港银行、香港信用卡、美股券商、数字货币、汇款、支付优化、返现活动、开户教程、保号教程、避坑经验、官方条款整理和实测攻略。用户要求使用 skill、按猫MOMO风格、完整文章、发布用 Markdown、配图、打包、一键下载、最终交付、小红书、卡片、多图、3:4、9:16、水印、平铺水印、防盗图、GitHub 交付、仓库配置或专用 KEY 配置时触发。
|
||||||
---
|
---
|
||||||
|
|
||||||
# MAOMOMO 文章生成器
|
# MAOMOMO 文章生成器
|
||||||
|
|
@ -17,6 +17,7 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
||||||
- 活动规则整理、开户教程、转账 / 入金 / 出金教程、保号方案、用卡姿势、对比总结、避坑经验。
|
- 活动规则整理、开户教程、转账 / 入金 / 出金教程、保号方案、用卡姿势、对比总结、避坑经验。
|
||||||
- 已有文章 SEO 优化、发布用 Markdown、配图规划、独立配图、ZIP 打包和后续改稿。
|
- 已有文章 SEO 优化、发布用 Markdown、配图规划、独立配图、ZIP 打包和后续改稿。
|
||||||
- 小红书卡片、多图信息卡、3:4 或 9:16 竖版 PNG 系列图、封面 / 内容卡 / 结尾卡分批交付。
|
- 小红书卡片、多图信息卡、3:4 或 9:16 竖版 PNG 系列图、封面 / 内容卡 / 结尾卡分批交付。
|
||||||
|
- 对最终配图批量或单张叠加 `MAOMOMO.COM` 45° 白色半透明平铺水印。
|
||||||
|
|
||||||
不适合:
|
不适合:
|
||||||
|
|
||||||
|
|
@ -50,6 +51,7 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
||||||
- 图片交付完成后必须生成或更新 `final_manifest.json`,最终推送、打包或链接交付只能读取 `final_manifest.json` 或状态文件里明确标记为 final 的 assets;不得把 `draft`、`v2`、旧比例、失败图或未通过 QA 的图一起交付。
|
- 图片交付完成后必须生成或更新 `final_manifest.json`,最终推送、打包或链接交付只能读取 `final_manifest.json` 或状态文件里明确标记为 final 的 assets;不得把 `draft`、`v2`、旧比例、失败图或未通过 QA 的图一起交付。
|
||||||
- 面向 Telegram / 聊天渠道的大文件交付默认改为 GitHub 分批推送:例如 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets`。Telegram 只负责通知进度和给 GitHub 文件路径、commit、release 或下载链接,不承载大 ZIP 或大图片包。
|
- 面向 Telegram / 聊天渠道的大文件交付默认改为 GitHub 分批推送:例如 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets`。Telegram 只负责通知进度和给 GitHub 文件路径、commit、release 或下载链接,不承载大 ZIP 或大图片包。
|
||||||
- 需要 GitHub 分批交付,或用户提到 GitHub 仓库、专用 KEY、token、PAT、deploy key 时,必须先读 `references/github-delivery.md`,引导配置专用私有交付仓库和最小权限凭据;不得要求用户在聊天里粘贴密钥,不得把 token / 私钥写入 prompt、manifest、状态文件、README、日志或提交记录。
|
- 需要 GitHub 分批交付,或用户提到 GitHub 仓库、专用 KEY、token、PAT、deploy key 时,必须先读 `references/github-delivery.md`,引导配置专用私有交付仓库和最小权限凭据;不得要求用户在聊天里粘贴密钥,不得把 token / 私钥写入 prompt、manifest、状态文件、README、日志或提交记录。
|
||||||
|
- 用户要求“水印”“平铺水印”“防盗图”“打水印”时,先读 `references/visual-assets.md` 的平铺水印规则;水印只处理最终 QA 通过的交付图或交付副本,默认保留无水印原图。水印必须用 `uv run python scripts/maomomo_tile_watermark.py` 生成,不得手工覆盖。
|
||||||
- 每张图都必须在状态文件记录:prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub。中断恢复时先读状态文件和 `final_manifest.json`,不要依赖聊天记忆。
|
- 每张图都必须在状态文件记录:prompt 文件、生成结果、QA 状态、是否进入 `assets/`、是否已推送 GitHub。中断恢复时先读状态文件和 `final_manifest.json`,不要依赖聊天记忆。
|
||||||
- 配图文件必须是独立 PNG;不要用一张大拼图代替多张图,不要只给提示词冒充图片文件。
|
- 配图文件必须是独立 PNG;不要用一张大拼图代替多张图,不要只给提示词冒充图片文件。
|
||||||
- 后续改稿必须更新文件本体;影响图片、来源区、标题、摘要或 ZIP 时同步更新。
|
- 后续改稿必须更新文件本体;影响图片、来源区、标题、摘要或 ZIP 时同步更新。
|
||||||
|
|
@ -137,6 +139,7 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
||||||
├── slide_run_state.json
|
├── slide_run_state.json
|
||||||
├── final_manifest.json
|
├── final_manifest.json
|
||||||
├── github_batches.json
|
├── github_batches.json
|
||||||
|
├── watermarked_assets/ # 可选,最终图的 MAOMOMO.COM 平铺水印版本
|
||||||
├── outline.md
|
├── outline.md
|
||||||
├── article.md
|
├── article.md
|
||||||
├── speech.md
|
├── speech.md
|
||||||
|
|
@ -149,6 +152,7 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
||||||
- `prompts/{article_slug}_XX.json` 记录每张图的文件名、用途、放置位置、核心文案、完整 prompt、依赖素材和输出路径。
|
- `prompts/{article_slug}_XX.json` 记录每张图的文件名、用途、放置位置、核心文案、完整 prompt、依赖素材和输出路径。
|
||||||
- `slide_jobs.json` 和 `slide_run_state.json` 记录每个图片 job 的 pending / dispatched / recorded / blocked 状态;使用 `scripts/maomomo_job_state.py` 初始化和记录状态。
|
- `slide_jobs.json` 和 `slide_run_state.json` 记录每个图片 job 的 pending / dispatched / recorded / blocked 状态;使用 `scripts/maomomo_job_state.py` 初始化和记录状态。
|
||||||
- `final_manifest.json` 只记录通过 QA 且进入 `assets/` 的最终交付文件;`github_batches.json` 记录每批 GitHub 推送的 batch id、文件路径、commit、release 或下载链接。
|
- `final_manifest.json` 只记录通过 QA 且进入 `assets/` 的最终交付文件;`github_batches.json` 记录每批 GitHub 推送的 batch id、文件路径、commit、release 或下载链接。
|
||||||
|
- 用户要求水印时,使用 `uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets` 生成水印副本;水印图可进入 GitHub 批次中的 `watermarked/`,但无水印原图仍保留。
|
||||||
- 小红书卡片项目没有文章正文时,可以省略 `article.md` 和 ZIP,但仍必须保留 `origin_image/`、`assets/`、`prompts/`、`slide_jobs.json`、`slide_run_state.json`、`final_manifest.json` 和 `github_batches.json`。
|
- 小红书卡片项目没有文章正文时,可以省略 `article.md` 和 ZIP,但仍必须保留 `origin_image/`、`assets/`、`prompts/`、`slide_jobs.json`、`slide_run_state.json`、`final_manifest.json` 和 `github_batches.json`。
|
||||||
|
|
||||||
8. 出图前整理全部 prompt。
|
8. 出图前整理全部 prompt。
|
||||||
|
|
@ -187,6 +191,7 @@ description: 生成或优化符合 maomomo / 猫MOMO 风格的中文出海金融
|
||||||
- `speech.md` 用作每张图的备注 / 讲解稿,标题格式使用 `## Slide N: 标题`,便于和图片顺序映射。
|
- `speech.md` 用作每张图的备注 / 讲解稿,标题格式使用 `## Slide N: 标题`,便于和图片顺序映射。
|
||||||
- 组装最终文章包时,把 `origin_image/{article_slug}_XX.png` 按顺序同步到 Markdown 引用的 `assets/`,并确保 `article.md`、图片路径、`speech.md`、来源区和 ZIP 都是最新版本。
|
- 组装最终文章包时,把 `origin_image/{article_slug}_XX.png` 按顺序同步到 Markdown 引用的 `assets/`,并确保 `article.md`、图片路径、`speech.md`、来源区和 ZIP 都是最新版本。
|
||||||
- 最终交付只能读取 `final_manifest.json` 或状态文件中 `qa_status=passed`、`entered_assets=true`、`final=true` 的文件,不扫描整个目录交付。
|
- 最终交付只能读取 `final_manifest.json` 或状态文件中 `qa_status=passed`、`entered_assets=true`、`final=true` 的文件,不扫描整个目录交付。
|
||||||
|
- 若交付水印版,在最终 QA 后用 `uv run python scripts/maomomo_tile_watermark.py` 对 final assets 生成 `watermarked_assets/`,并在 `final_manifest.json` 记录 `watermark` 字段:text、angle、color、alpha、output_dir。
|
||||||
- 需要 GitHub 分批交付时,先按 `references/github-delivery.md` 确认交付仓库、认证方式、目标路径和仓库隐私级别;缺配置时先给用户本机配置步骤并暂停 push。配置就绪后按 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets` 等批次推送;每批推送后记录 GitHub 文件路径、commit、release 或下载链接,并写入 `github_batches.json` / 状态文件。
|
- 需要 GitHub 分批交付时,先按 `references/github-delivery.md` 确认交付仓库、认证方式、目标路径和仓库隐私级别;缺配置时先给用户本机配置步骤并暂停 push。配置就绪后按 `batch-01-cover`、`batch-02-content-cards`、`batch-03-final-assets` 等批次推送;每批推送后记录 GitHub 文件路径、commit、release 或下载链接,并写入 `github_batches.json` / 状态文件。
|
||||||
- 完整交付、文件交付或改稿打包前读 `references/delivery-rules.md`。
|
- 完整交付、文件交付或改稿打包前读 `references/delivery-rules.md`。
|
||||||
- QA 必须用脚本检查 Markdown 中所有图片路径真实存在,并检查 assets 是否存在、来源区只保留实际使用来源、正文无编辑痕迹、图片无错误日期 / 金额 / 未确认结论、用户要求删除内容不残留、ZIP 内容正确。
|
- QA 必须用脚本检查 Markdown 中所有图片路径真实存在,并检查 assets 是否存在、来源区只保留实际使用来源、正文无编辑痕迹、图片无错误日期 / 金额 / 未确认结论、用户要求删除内容不残留、ZIP 内容正确。
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,42 @@
|
||||||
- 每张图的可见文字必须受控:只允许出现该图指定的业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,不得添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。
|
- 每张图的可见文字必须受控:只允许出现该图指定的业务文案、`MAOMOMO` 和已核对允许出现的品牌 / 支付标识,不得添加副标题、风格标签、prompt 描述、说明文字、额外 slogan 或无关文字。
|
||||||
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代图片生成后端。
|
- 不要用 Playwright、HTML/CSS、SVG、Pillow、canvas、本地拼贴或手工覆盖文字替代图片生成后端。
|
||||||
|
|
||||||
|
|
||||||
|
## 平铺水印
|
||||||
|
|
||||||
|
用户要求“水印”“平铺水印”“防盗图”“打水印”时,对最终配图生成水印副本。默认规格:
|
||||||
|
|
||||||
|
- 水印文字:`MAOMOMO.COM`
|
||||||
|
- 角度:45°
|
||||||
|
- 颜色:白色 `#FFFFFF`
|
||||||
|
- 透明度:半透明,默认 alpha `72/255`
|
||||||
|
- 排列:对角线平铺
|
||||||
|
- 工具:`uv run python scripts/maomomo_tile_watermark.py`
|
||||||
|
|
||||||
|
执行示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python scripts/maomomo_tile_watermark.py \
|
||||||
|
--input assets \
|
||||||
|
--output watermarked_assets
|
||||||
|
```
|
||||||
|
|
||||||
|
单张图:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python scripts/maomomo_tile_watermark.py \
|
||||||
|
--input assets/example.png \
|
||||||
|
--output watermarked_assets/example.png
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- 水印只加到最终 QA 通过的 `assets/` 或交付副本;不要在样张确认前加水印,避免影响用户判断画面文字质量。
|
||||||
|
- 默认保留无水印原图在 `origin_image/` 和 / 或 `assets/`;水印图输出到 `watermarked_assets/`,或 GitHub 交付批次中的 `watermarked/`。
|
||||||
|
- 不要对官方截图证据原图、需要继续发给图片模型编辑的素材、尚未 QA 的图直接覆盖水印。
|
||||||
|
- `final_manifest.json` 应记录 `watermark` 字段,至少包含 `text`、`angle`、`color`、`alpha`、`output_dir` 和水印文件清单。
|
||||||
|
- 交付回复中说明是否提供水印版,以及无水印原图是否保留。
|
||||||
|
|
||||||
## 可见文字与 prompt 边界
|
## 可见文字与 prompt 边界
|
||||||
|
|
||||||
- 视觉风格只作为后台画面描述,不得成为画面文字。
|
- 视觉风格只作为后台画面描述,不得成为画面文字。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""为 MAOMOMO 配图叠加 45° 平铺半透明水印。
|
||||||
|
|
||||||
|
默认水印:MAOMOMO.COM,白色半透明,45° 对角线平铺。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
uv run python scripts/maomomo_tile_watermark.py --input assets --output watermarked_assets
|
||||||
|
uv run python scripts/maomomo_tile_watermark.py --input assets/example.png --output assets/example-watermarked.png
|
||||||
|
|
||||||
|
说明:
|
||||||
|
脚本使用 Pillow。本机 Python 没有 Pillow 且存在 uv 时,会自动用
|
||||||
|
`uv run --with pillow python ...` 重启一次,不需要把依赖写入项目。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Iterable, Iterator, Optional, Tuple, Any
|
||||||
|
|
||||||
|
SUPPORTED_EXTS = {".png", ".jpg", ".jpeg", ".webp"}
|
||||||
|
|
||||||
|
|
||||||
|
def die(message: str, code: int = 1) -> None:
|
||||||
|
print(f"错误:{message}", file=sys.stderr)
|
||||||
|
raise SystemExit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_pillow() -> Tuple[Any, Any, Any]:
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
return Image, ImageDraw, ImageFont
|
||||||
|
except ModuleNotFoundError as exc:
|
||||||
|
if exc.name != "PIL":
|
||||||
|
raise
|
||||||
|
uv = shutil.which("uv")
|
||||||
|
if uv and os.environ.get("MAOMOMO_WATERMARK_REEXEC") != "1":
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["MAOMOMO_WATERMARK_REEXEC"] = "1"
|
||||||
|
cmd = [uv, "run", "--with", "pillow>=10.0.0", "python", str(Path(__file__).resolve()), *sys.argv[1:]]
|
||||||
|
raise SystemExit(subprocess.call(cmd, env=env))
|
||||||
|
die("缺少 Pillow。请使用 `uv run python scripts/maomomo_tile_watermark.py ...` 运行,或安装 Pillow。")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_color(value: str) -> Tuple[int, int, int]:
|
||||||
|
text = value.strip()
|
||||||
|
if text.startswith("#"):
|
||||||
|
text = text[1:]
|
||||||
|
if len(text) != 6:
|
||||||
|
die("颜色必须是 #RRGGBB,例如 #FFFFFF")
|
||||||
|
try:
|
||||||
|
return tuple(int(text[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
|
||||||
|
except ValueError:
|
||||||
|
die("颜色必须是有效十六进制,例如 #FFFFFF")
|
||||||
|
|
||||||
|
|
||||||
|
def load_font(ImageFont: Any, font_path: Optional[str], font_size: int) -> Any:
|
||||||
|
candidates = []
|
||||||
|
if font_path:
|
||||||
|
candidates.append(Path(font_path).expanduser())
|
||||||
|
candidates.extend(
|
||||||
|
Path(p)
|
||||||
|
for p in [
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.exists():
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype(str(candidate), font_size)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def iter_images(input_path: Path) -> Iterator[Path]:
|
||||||
|
if input_path.is_file():
|
||||||
|
if input_path.suffix.lower() not in SUPPORTED_EXTS:
|
||||||
|
die(f"不支持的图片格式:{input_path}")
|
||||||
|
yield input_path
|
||||||
|
return
|
||||||
|
if not input_path.is_dir():
|
||||||
|
die(f"输入不存在:{input_path}")
|
||||||
|
for path in sorted(input_path.rglob("*")):
|
||||||
|
if path.is_file() and path.suffix.lower() in SUPPORTED_EXTS:
|
||||||
|
yield path
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output(input_root: Path, image_path: Path, output_root: Path) -> Path:
|
||||||
|
if input_root.is_file():
|
||||||
|
return output_root
|
||||||
|
rel = image_path.relative_to(input_root)
|
||||||
|
return output_root / rel
|
||||||
|
|
||||||
|
|
||||||
|
def make_tile(
|
||||||
|
Image: Any,
|
||||||
|
ImageDraw: Any,
|
||||||
|
text: str,
|
||||||
|
font: Any,
|
||||||
|
color: Tuple[int, int, int],
|
||||||
|
alpha: int,
|
||||||
|
angle: float,
|
||||||
|
padding: int,
|
||||||
|
) -> Any:
|
||||||
|
dummy = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(dummy)
|
||||||
|
bbox = draw.textbbox((0, 0), text, font=font)
|
||||||
|
text_w = bbox[2] - bbox[0]
|
||||||
|
text_h = bbox[3] - bbox[1]
|
||||||
|
tile_w = max(1, text_w + padding * 2)
|
||||||
|
tile_h = max(1, text_h + padding * 2)
|
||||||
|
tile = Image.new("RGBA", (tile_w, tile_h), (0, 0, 0, 0))
|
||||||
|
tile_draw = ImageDraw.Draw(tile)
|
||||||
|
tile_draw.text((padding, padding), text, font=font, fill=(*color, alpha))
|
||||||
|
return tile.rotate(angle, expand=True, resample=Image.Resampling.BICUBIC)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_watermark(
|
||||||
|
image_path: Path,
|
||||||
|
output_path: Path,
|
||||||
|
*,
|
||||||
|
text: str,
|
||||||
|
angle: float,
|
||||||
|
alpha: int,
|
||||||
|
color: Tuple[int, int, int],
|
||||||
|
font_size: int,
|
||||||
|
font_path: Optional[str],
|
||||||
|
spacing: int,
|
||||||
|
quality: int,
|
||||||
|
) -> None:
|
||||||
|
if alpha < 0 or alpha > 255:
|
||||||
|
die("alpha 必须在 0-255 之间")
|
||||||
|
if spacing < 0:
|
||||||
|
die("spacing 不能为负数")
|
||||||
|
Image, ImageDraw, ImageFont = ensure_pillow()
|
||||||
|
font = load_font(ImageFont, font_path, font_size)
|
||||||
|
with Image.open(image_path) as src:
|
||||||
|
base = src.convert("RGBA")
|
||||||
|
width, height = base.size
|
||||||
|
diagonal = int((width * width + height * height) ** 0.5)
|
||||||
|
tile = make_tile(Image, ImageDraw, text, font, color, alpha, angle, spacing)
|
||||||
|
overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||||
|
step_x = max(1, tile.width + spacing)
|
||||||
|
step_y = max(1, tile.height + spacing)
|
||||||
|
start_x = -diagonal
|
||||||
|
start_y = -diagonal
|
||||||
|
for y in range(start_y, height + diagonal, step_y):
|
||||||
|
for x in range(start_x, width + diagonal, step_x):
|
||||||
|
overlay.alpha_composite(tile, (x, y))
|
||||||
|
out = Image.alpha_composite(base, overlay)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
suffix = output_path.suffix.lower()
|
||||||
|
if suffix in {".jpg", ".jpeg"}:
|
||||||
|
out = out.convert("RGB")
|
||||||
|
out.save(output_path, quality=quality, optimize=True)
|
||||||
|
elif suffix == ".webp":
|
||||||
|
out.save(output_path, quality=quality, method=6)
|
||||||
|
else:
|
||||||
|
out.save(output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(description="为图片叠加 MAOMOMO.COM 45° 平铺半透明水印。")
|
||||||
|
parser.add_argument("--input", required=True, help="输入图片文件或目录。")
|
||||||
|
parser.add_argument("--output", required=True, help="输出图片文件或目录。目录模式会保持相对路径。")
|
||||||
|
parser.add_argument("--text", default="MAOMOMO.COM", help="水印文字,默认 MAOMOMO.COM。")
|
||||||
|
parser.add_argument("--angle", type=float, default=45.0, help="水印旋转角度,默认 45。")
|
||||||
|
parser.add_argument("--alpha", type=int, default=72, help="透明度 0-255,默认 72。")
|
||||||
|
parser.add_argument("--color", default="#FFFFFF", help="水印颜色,默认 #FFFFFF。")
|
||||||
|
parser.add_argument("--font-size", type=int, default=42, help="字体大小,默认 42。")
|
||||||
|
parser.add_argument("--font", default=None, help="可选字体路径。")
|
||||||
|
parser.add_argument("--spacing", type=int, default=120, help="水印间距 / 内边距,默认 120。")
|
||||||
|
parser.add_argument("--quality", type=int, default=95, help="JPG/WEBP 输出质量,默认 95。")
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[Iterable[str]] = None) -> int:
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args(list(argv) if argv is not None else None)
|
||||||
|
input_path = Path(args.input).expanduser().resolve()
|
||||||
|
output_path = Path(args.output).expanduser().resolve()
|
||||||
|
color = parse_color(args.color)
|
||||||
|
images = list(iter_images(input_path))
|
||||||
|
if not images:
|
||||||
|
die(f"没有找到支持的图片:{input_path}")
|
||||||
|
if input_path.is_dir() and output_path.exists() and output_path.is_file():
|
||||||
|
die("输入为目录时,输出必须是目录路径")
|
||||||
|
results = []
|
||||||
|
for image_path in images:
|
||||||
|
target = resolve_output(input_path, image_path, output_path)
|
||||||
|
apply_watermark(
|
||||||
|
image_path,
|
||||||
|
target,
|
||||||
|
text=args.text,
|
||||||
|
angle=args.angle,
|
||||||
|
alpha=args.alpha,
|
||||||
|
color=color,
|
||||||
|
font_size=args.font_size,
|
||||||
|
font_path=args.font,
|
||||||
|
spacing=args.spacing,
|
||||||
|
quality=args.quality,
|
||||||
|
)
|
||||||
|
results.append((image_path, target))
|
||||||
|
for src, dst in results:
|
||||||
|
print(f"watermarked: {src} -> {dst}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Loading…
Reference in New Issue