新增 PDF/EPUB/MOBI/AZW 内置阅读器,PDF 分段读取支持超大文件, 批注、读书与画布笔记、封面生成与本地导入。AI 助手支持三种协议、 图像上下文与安全 Markdown 渲染,上下文范围改为 选中/当前页/全文, 页面与全文无需选中文本即可发送,全文会提示可能超出模型限制。 便携版输出目录固定为 PeopleLib-windows-x64,不再随版本号变化, 避免升级后 data/ 被遗留在旧目录。 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
96 lines
3.0 KiB
Python
96 lines
3.0 KiB
Python
"""Crop the source renders to the icon tile and export PNG sizes plus .ico files."""
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
ROOT = "D:/my_git/peoplelib/icons"
|
|
SIZES = [1024, 512, 256, 128, 64, 48, 32, 16]
|
|
ICO_SIZES = [256, 128, 64, 48, 32, 16]
|
|
SS = 4
|
|
|
|
SOURCES = [
|
|
("light", f"{ROOT}/ChatGPT_0CjIH8EmAj.png"),
|
|
("dark", f"{ROOT}/ChatGPT_RAga3pG7De.png"),
|
|
]
|
|
|
|
|
|
def tile_mask(rgb):
|
|
a = rgb.astype(int)
|
|
r, g, b = a[..., 0], a[..., 1], a[..., 2]
|
|
# Anything that is not the near-white page background belongs to the artwork.
|
|
return (a.sum(2) < 748) | (np.abs(r - b) > 4) | (np.abs(g - b) > 4)
|
|
|
|
|
|
def tile_bbox(mask):
|
|
cols = mask.sum(0)
|
|
rows = mask.sum(1)
|
|
xs = np.where(cols > cols.max() * 0.35)[0]
|
|
ys = np.where(rows > rows.max() * 0.35)[0]
|
|
x0, x1, y0, y1 = xs.min(), xs.max(), ys.min(), ys.max()
|
|
# The renders carry a drop shadow below the tile, so trust the width and
|
|
# square the crop downward from the top edge.
|
|
side = x1 - x0 + 1
|
|
return x0, x1, y0, y0 + side - 1
|
|
|
|
|
|
def corner_radius(mask, x0, x1, y0, y1):
|
|
ests = []
|
|
for d in range(10, int((y1 - y0) * 0.18)):
|
|
idx = np.where(mask[y0 + d, x0:x1 + 1])[0]
|
|
if not len(idx) or idx.min() <= 0:
|
|
continue
|
|
x = float(idx.min())
|
|
ests.append((d + x) + np.sqrt(2.0 * d * x))
|
|
return float(np.median(ests)) if ests else (x1 - x0) * 0.21
|
|
|
|
|
|
def rounded_alpha(w, h, radius, size):
|
|
n = size * SS
|
|
ys, xs = np.mgrid[0:n, 0:n].astype(np.float64)
|
|
# Map supersampled pixel centres back onto the source tile grid.
|
|
px = (xs + 0.5) / n * w
|
|
py = (ys + 0.5) / n * h
|
|
r = radius
|
|
dx = np.clip(r - px, 0, None) + np.clip(px - (w - r), 0, None)
|
|
dy = np.clip(r - py, 0, None) + np.clip(py - (h - r), 0, None)
|
|
inside = (dx * dx + dy * dy) <= r * r
|
|
cov = inside.reshape(size, SS, size, SS).mean((1, 3))
|
|
return (cov * 255).round().astype(np.uint8)
|
|
|
|
|
|
def build(name, path):
|
|
src = Image.open(path).convert("RGB")
|
|
mask = tile_mask(np.asarray(src))
|
|
x0, x1, y0, y1 = tile_bbox(mask)
|
|
radius = corner_radius(mask, x0, x1, y0, y1)
|
|
tile = src.crop((x0, y0, x1 + 1, y1 + 1))
|
|
w, h = tile.size
|
|
print(f"{name}: tile {w}x{h} radius {radius:.1f}")
|
|
|
|
outdir = f"{ROOT}/dist/{name}"
|
|
import os
|
|
os.makedirs(outdir, exist_ok=True)
|
|
|
|
frames = {}
|
|
for size in SIZES:
|
|
img = tile.resize((size, size), Image.LANCZOS).convert("RGBA")
|
|
img.putalpha(Image.fromarray(rounded_alpha(w, h, radius, size), "L"))
|
|
img.save(f"{outdir}/icon-{size}.png", optimize=True)
|
|
frames[size] = img
|
|
|
|
frames[1024].save(f"{ROOT}/dist/book-ai-{name}.ico", format="ICO",
|
|
sizes=[(s, s) for s in ICO_SIZES])
|
|
return frames[512]
|
|
|
|
|
|
def main():
|
|
previews = [build(n, p) for n, p in SOURCES]
|
|
gap = 32
|
|
sheet = Image.new("RGBA", (512 * 2 + gap * 3, 512 + gap * 2), (128, 128, 128, 255))
|
|
for i, img in enumerate(previews):
|
|
sheet.paste(img, (gap + i * (512 + gap), gap), img)
|
|
sheet.save(f"{ROOT}/dist/preview.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|