From e44b7fc191a54e85c6f2cf20969e9e92c5893a1e Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 18:53:00 +0800 Subject: [PATCH 1/7] Add tools/watermark.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 给配图加 CyC2018 浅水印:整张图斜向平铺淡色文字,右下角加与已有配图一致的标识。加过水印的图片会写入元数据标记,重复执行时跳过;--check 只检查不修改。 --- tools/watermark.py | 161 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tools/watermark.py diff --git a/tools/watermark.py b/tools/watermark.py new file mode 100644 index 00000000..6f34a360 --- /dev/null +++ b/tools/watermark.py @@ -0,0 +1,161 @@ +"""给配图加 CyC2018 浅水印。 + +用法: + python3 tools/watermark.py notes/pics/a.png notes/pics/b.jpg + python3 tools/watermark.py --check notes/pics/*.png # 只检查,不修改 + +水印分两层: +- 整张图斜向平铺一层很淡的 “CyC2018” 文字,不影响阅读; +- 右下角加上与已有配图一致的浅灰色猫头图标和 “CyC2018”,右下角有内容时在底部补白边。 +角标大小按图片宽度自动缩放,也可以用 --scale 指定。 + +加过水印的图片会在元数据中写入标记,再次执行时直接跳过,不会重复叠加。 +GIF 动图不处理。依赖 Pillow。 +""" +import argparse +import os +import sys + +from PIL import Image, ImageDraw, ImageFont, PngImagePlugin + +TEXT = "CyC2018" +MARK = "Watermark: CyC2018" +GRAY = (128, 128, 128) +ALPHA = 0.57 # 角标:白底上约为 #B7B7B7,与已有配图的水印深浅一致 +TILE_ALPHA = 0.07 # 平铺:白底上约为 #F6F6F6,只在留意时可见 +TILE_ANGLE = 30 # 平铺文字的倾斜角度 +SS = 4 # 超采样倍数,用于抗锯齿 + +FONTS = [ # 等宽字体,与已有水印一致 + "/System/Library/Fonts/Menlo.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "C:/Windows/Fonts/consola.ttf", +] + + +def _font(size): + for path in FONTS: + if os.path.exists(path): + return ImageFont.truetype(path, size) + return ImageFont.load_default(size) + + +def _mark(scale): + """生成右下角水印的 alpha 蒙版。""" + icon = round(24 * scale) * SS + font = _font(round(19 * scale) * SS) + left, top, right, bottom = font.getbbox(TEXT) + gap = round(8 * scale) * SS + w = icon + gap + right + h = max(icon, bottom - top) + mask = Image.new("L", (w, h), 0) + d = ImageDraw.Draw(mask) + + # 猫头图标:按 100×100 网格绘制后缩放 + def p(x, y): + return (x * icon / 100, (h - icon) / 2 + y * icon / 100) + + d.polygon([p(10, 50), p(14, 4), p(46, 26)], fill=255) + d.polygon([p(90, 50), p(86, 4), p(54, 26)], fill=255) + d.rounded_rectangle([p(6, 22), p(94, 96)], radius=34 * icon / 100, fill=255) + d.rounded_rectangle([p(18, 54), p(82, 88)], radius=16 * icon / 100, fill=0) + d.ellipse([p(31, 61), p(41, 79)], fill=255) + d.ellipse([p(59, 61), p(69, 79)], fill=255) + + d.text((icon + gap, (h - (bottom - top)) / 2 - top), TEXT, font=font, fill=255) + return mask.resize((w // SS, h // SS), Image.LANCZOS) + + +def _tiles(size, scale): + """生成整张图斜向平铺的文字蒙版。""" + W, H = size + font = _font(round(15 * scale)) + right = font.getbbox(TEXT)[2] + dx, dy = round(right * 2.6), round(85 * scale) + side = int((W ** 2 + H ** 2) ** 0.5) + 2 * dx + big = Image.new("L", (side, side), 0) + d = ImageDraw.Draw(big) + for row, y in enumerate(range(0, side, dy)): + for x in range(-dx + (dx // 2) * (row % 2), side, dx): + d.text((x, y), TEXT, font=font, fill=255) + big = big.rotate(TILE_ANGLE, resample=Image.BICUBIC) + l, t = (side - W) // 2, (side - H) // 2 + return big.crop((l, t, l + W, t + H)) + + +def _blank(img, box): + """判断区域内是否基本为空白。""" + region = img.crop(box).convert("L") + dark = sum(1 for v in region.getdata() if v < 235) + return dark <= region.width * region.height * 0.002 + + +def add(img, scale=None): + """返回加了水印的新图片。scale 默认按宽度计算,1000px 宽的图为 1。""" + if scale is None: + scale = min(max(img.width / 1000, 0.6), 2.0) + img = img.convert("RGBA") if img.mode in ("P", "LA", "RGBA") else img.convert("RGB") + mask = _mark(scale) + mx, my = round(12 * scale), round(6 * scale) + W, H = img.size + box = (W - mask.width - 2 * mx, H - mask.height - 2 * my, W, H) + if box[0] < 0 or not _blank(img, box): + bg = (255, 255, 255, 255) if img.mode == "RGBA" else (255, 255, 255) + padded = Image.new(img.mode, (W, H + mask.height + 2 * my), bg) + padded.paste(img, (0, 0)) + img, H = padded, padded.height + layer = Image.new(img.mode, img.size, GRAY + ((255,) if img.mode == "RGBA" else ())) + img.paste(layer, (0, 0), _tiles(img.size, scale).point(lambda v: round(v * TILE_ALPHA))) + corner = Image.new("L", img.size, 0) + corner.paste(mask.point(lambda v: round(v * ALPHA)), (W - mask.width - mx, H - mask.height - my)) + img.paste(layer, (0, 0), corner) + return img + + +def marked(img): + """图片是否已经加过水印。""" + value = img.info.get("Watermark") or img.info.get("comment") or b"" + if isinstance(value, bytes): + value = value.decode("utf-8", "ignore") + return "CyC2018" in value + + +def save(img, path): + """保存图片并写入水印标记。""" + if path.lower().endswith(".png"): + info = PngImagePlugin.PngInfo() + info.add_text("Watermark", "CyC2018") + img.save(path, pnginfo=info, optimize=True) + else: + img.convert("RGB").save(path, quality=92, comment=MARK.encode()) + + +def main(): + ap = argparse.ArgumentParser(description="给配图加 CyC2018 浅水印") + ap.add_argument("images", nargs="+") + ap.add_argument("--scale", type=float, help="角标缩放,默认按图片宽度计算") + ap.add_argument("--check", action="store_true", help="只检查是否已加水印") + ap.add_argument("--mark-only", action="store_true", help="图片已有水印,只写入标记") + args = ap.parse_args() + missing = 0 + for path in args.images: + if not path.lower().endswith((".png", ".jpg", ".jpeg")): + print("skip (not png/jpg)", path) + continue + img = Image.open(path) + if marked(img): + print("ok", path) + elif args.check: + print("missing watermark", path) + missing += 1 + elif args.mark_only: + save(img, path) + print("marked", path) + else: + save(add(img, args.scale), path) + print("watermarked", path) + sys.exit(1 if missing else 0) + + +if __name__ == "__main__": + main() From a2be2bc989603c8add08bb5068ad80ab342070fa Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 18:53:00 +0800 Subject: [PATCH 2/7] Add tools/hooks/pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启用 git config core.hooksPath tools/hooks 后,提交时自动给 notes/pics 下新增或修改的 PNG、JPEG 图片加水印。 --- tools/hooks/pre-commit | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100755 tools/hooks/pre-commit diff --git a/tools/hooks/pre-commit b/tools/hooks/pre-commit new file mode 100755 index 00000000..17e85393 --- /dev/null +++ b/tools/hooks/pre-commit @@ -0,0 +1,14 @@ +#!/bin/sh +# 提交时给 notes/pics 下新增或修改的 PNG、JPEG 配图自动加 CyC2018 浅水印。 +# 启用:git config core.hooksPath tools/hooks +root=$(git rev-parse --show-toplevel) || exit 1 +files=$(git diff --cached --name-only --diff-filter=AM -- 'notes/pics/*.png' 'notes/pics/*.jpg' 'notes/pics/*.jpeg') +[ -z "$files" ] && exit 0 +if ! python3 -c "import PIL" 2>/dev/null; then + echo "pre-commit: 需要 Pillow 才能给配图加水印,请先执行 pip3 install pillow" >&2 + exit 1 +fi +echo "$files" | while IFS= read -r f; do + python3 "$root/tools/watermark.py" "$root/$f" || exit 1 + git add -- "$root/$f" +done From 6b7b3d9824ef415027a3e481d380749aab4acf5e Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 18:53:00 +0800 Subject: [PATCH 3/7] Add tools/diagram/build.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 tools/diagram/src 下的 SVG 用无头浏览器渲染成 notes/pics 下的 2 倍分辨率 PNG,让示意图有可编辑的源文件。 --- tools/diagram/build.py | 70 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tools/diagram/build.py diff --git a/tools/diagram/build.py b/tools/diagram/build.py new file mode 100644 index 00000000..8ccc0a49 --- /dev/null +++ b/tools/diagram/build.py @@ -0,0 +1,70 @@ +"""把 src/*.svg 渲染成 notes/pics/*.png。 + +用法: + python3 tools/diagram/build.py # 渲染全部 + python3 tools/diagram/build.py paxos-example cache-aside + +依赖:Pillow,以及 Chrome、Edge 或 Chromium 之一(无头模式截图)。 +浏览器路径可用环境变量 CHROME 指定。 + +图片按 2 倍分辨率输出,在笔记里用 width 指定为 SVG 的宽度即可。 +渲染结果不带水印,水印由 tools/watermark.py 添加,启用 tools/hooks 后提交时会自动执行。 +""" +import os +import shutil +import subprocess +import sys +import tempfile +import xml.etree.ElementTree as ET + +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, "src") +OUT = os.path.join(HERE, "..", "..", "notes", "pics") +SCALE = 2 # 按 2 倍分辨率输出,在高分屏上也清晰 + +BROWSERS = [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "google-chrome", "chromium", "chromium-browser", "microsoft-edge", +] + + +def browser(): + if os.environ.get("CHROME"): + return os.environ["CHROME"] + for b in BROWSERS: + path = b if os.path.isabs(b) else shutil.which(b) + if path and os.path.exists(path): + return path + sys.exit("找不到 Chrome/Edge/Chromium,请用环境变量 CHROME 指定浏览器路径") + + +def render(name, exe, profile): + svg = os.path.join(SRC, name + ".svg") + root = ET.parse(svg).getroot() + w, h = int(float(root.get("width"))), int(float(root.get("height"))) + png = os.path.abspath(os.path.join(OUT, name + ".png")) + subprocess.run([ + exe, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--no-first-run", + "--user-data-dir=" + profile, f"--force-device-scale-factor={SCALE}", + f"--window-size={w},{h}", "--default-background-color=ffffffff", + "--screenshot=" + png, "file://" + os.path.abspath(svg), + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + img = Image.open(png).convert("RGB") + img.save(png, optimize=True) + print(f"{name}.svg -> notes/pics/{name}.png {img.size[0]}x{img.size[1]}") + + +def main(): + names = sys.argv[1:] or sorted(f[:-4] for f in os.listdir(SRC) if f.endswith(".svg")) + exe = browser() + with tempfile.TemporaryDirectory() as profile: + for name in names: + render(name, exe, profile) + + +if __name__ == "__main__": + main() From 8ce00b56107d15850b0a866f1da1c2db3049e75f Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 18:53:00 +0800 Subject: [PATCH 4/7] Add CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 说明提交 Issue、PR 的约定,以及配图的存放位置、引用方式和加水印的方法。 --- CONTRIBUTING.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fd3f9da4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# 贡献指南 + +本仓库是面向复习和面试的技术笔记,内容力求简洁准确。欢迎通过 Issue 或 PR 指出错误、补充内容。 + +## Issue + +指出错误时请写明: + +- 文件名和原文; +- 问题所在以及修改建议; +- 依据,优先引用官方文档、规范、论文或源码。 + +## PR + +- 一个 PR 只解决一类问题,例如修正错误和补充内容分开提交; +- 不改动已有的章节结构、标题编号和图片,修改标题时同步更新文件开头的目录; +- 排版遵循 [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines/blob/master/README.zh-Hans.md),中英文之间加空格,使用全角标点; +- 修改代码时在 PR 中说明验证方式,例如测试用例或与暴力解法的对照结果。 + +## 配图 + +新图片放在 `notes/pics` 目录,在笔记中用相对路径引用: + +```html +

+``` + +配图需要加 CyC2018 浅水印,包括整张图斜向平铺的淡色文字和右下角的标识。启用仓库自带的 Git 钩子后,提交时会自动给 `notes/pics` 下新增或修改的 PNG、JPEG 图片加水印,已加过的图片会跳过: + +```bash +git config core.hooksPath tools/hooks +``` + +也可以手动执行,依赖 Pillow: + +```bash +python3 tools/watermark.py notes/pics/xxx.png +python3 tools/watermark.py --check notes/pics/xxx.png # 只检查 +``` + +示意图建议保留可编辑的源文件。`tools/diagram/src` 中的 SVG 可以用以下命令渲染成 `notes/pics` 下同名的 PNG,需要本机安装 Chrome、Edge 或 Chromium: + +```bash +python3 tools/diagram/build.py xxx +``` From 02843d09a7cf29b2b8efdae2d9dfc8cb8e85fa57 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 18:53:00 +0800 Subject: [PATCH 5/7] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在排版说明后链接贡献指南。 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d5764058..4919c72f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ 在线排版工具:[Text-Typesetting](https://github.com/CyC2018/Text-Typesetting)。 +提交 Issue、PR 以及添加配图的约定见 [贡献指南](CONTRIBUTING.md)。 + ### License 本仓库的内容不是将网上的资料随意拼凑而来,除了少部分引用书上和技术文档的原文(这部分内容都在末尾的参考链接中加了出处),其余都是我的原创。在您引用本仓库内容或者对内容进行修改演绎时,请署名并以相同方式共享,谢谢。 From 28833068e3cdb098968c14a7db94c5a78c752555 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 19:04:33 +0800 Subject: [PATCH 6/7] Add .github/workflows/watermark.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR 中新增或修改了 notes/pics 下的 PNG、JPEG 配图时,检查是否已加 CyC2018 水印,作为未启用提交钩子时的兜底。 --- .github/workflows/watermark.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/watermark.yml diff --git a/.github/workflows/watermark.yml b/.github/workflows/watermark.yml new file mode 100644 index 00000000..b2f31e4b --- /dev/null +++ b/.github/workflows/watermark.yml @@ -0,0 +1,32 @@ +# 检查 PR 中新增或修改的配图是否已加 CyC2018 水印,缺少时提示用 tools/watermark.py 补上。 +name: 配图水印检查 + +on: + pull_request: + paths: + - 'notes/pics/**' + +permissions: + contents: read + +jobs: + watermark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install pillow + - name: 检查配图 + env: + BASE_REF: ${{ github.base_ref }} + run: | + git diff --name-only --diff-filter=AM "origin/$BASE_REF...HEAD" -- 'notes/pics/*.png' 'notes/pics/*.jpg' 'notes/pics/*.jpeg' > files.txt + if [ ! -s files.txt ]; then echo "没有新增或修改的 PNG、JPEG 配图"; exit 0; fi + if ! xargs -d '\n' python3 tools/watermark.py --check < files.txt; then + echo "::error::以上配图缺少水印,请执行 python3 tools/watermark.py <图片> 后重新提交,或启用 git config core.hooksPath tools/hooks" + exit 1 + fi From 8d203ceb48612bbc5653d7473dbcfce7da00f00a Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Sun, 25 Oct 2026 19:04:34 +0800 Subject: [PATCH 7/7] Update CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 说明配图水印的 CI 检查。 --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd3f9da4..d7e77d5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,8 @@ git config core.hooksPath tools/hooks ``` +PR 中新增或修改的配图如果没有水印,GitHub Actions 的“配图水印检查”会失败并给出提示。 + 也可以手动执行,依赖 Pillow: ```bash