Files
catalog_doc/scripts/make_icon.py
Andrey Kruchinkin 49f91247c7 Каталогизатор документов: разбор папки, тезисы, поиск, GUI
Программа принимает папку с документами, извлекает из них текст и
метаданные и строит каталог: краткое описание, тезисы и ключевые слова
для каждого файла плюс поиск по всему собранному.

Ядро:
- extract: PDF, DJVU, DOC(X), RTF, ODT, CHM, EPUB, FB2, PPT(X), XLS(X), TXT;
  PDF передаётся MuPDF потоком в память (работают длинные пути) и ограничен
  по времени — повреждённый файл иначе чинится минутами;
- summarize: экстрактивное реферирование по TF-IDF, посчитанному на самой
  библиотеке, с лёгким стеммером для русского и английского;
- db: SQLite с полнотекстовым индексом FTS5, морфологический поиск с BM25;
- scanner: инкрементальный многопоточный обход, счётчик попыток защищает
  от файла, обрывающего разбор;
- report: автономный HTML с поиском, Markdown, JSON, CSV.

Интерфейс:
- окно PySide6 в тёмной теме: вкладки разбора и поиска, фоновый поток,
  карточка документа, правка своих ключевых слов;
- командная строка: build, scan, analyze, report, search, show, tag, stats;
- у каждой папки свой каталог, последняя обработанная запоминается.

Сборка: scripts/build_exe.py даёт Catalogizer.exe в Windows и
Catalogizer.app в macOS, иконки .ico и .icns рисуются кодом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:20:29 +03:00

168 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""@file make_icon.py
@brief Генерация иконок программы без внешних библиотек.
Иконка рисуется кодом (стопка страниц с корешком и лупой), чтобы в
репозитории не заводить бинарный файл неизвестного происхождения.
Делаются оба формата: ``ico/catalog.ico`` для Windows и
``ico/catalog.icns`` для macOS.
Запуск: ``python scripts/make_icon.py``.
"""
from __future__ import annotations
import struct
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TARGET_ICO = ROOT / "ico" / "catalog.ico"
TARGET_ICNS = ROOT / "ico" / "catalog.icns"
#: Размеры изображений внутри .ico.
SIZES = (16, 24, 32, 48, 64, 128, 256)
#: Типы изображений ICNS и их размеры: macOS берёт подходящий сам.
ICNS_TYPES = ((b"ic11", 32), (b"ic12", 64), (b"ic07", 128),
(b"ic08", 256), (b"ic09", 512))
#: Служебные байты растровых форматов.
TRANSPARENT = bytes(4) # прозрачный пиксель
FILTER_NONE = bytes(1) # PNG: строка без фильтра
PNG_MAGIC = bytes((137, 80, 78, 71, 13, 10, 26, 10))
BG = (0x1d, 0x28, 0x38) # тёмная плашка в цветах темы
PAGE = (0xdc, 0xe6, 0xf2) # страница
PAGE_DIM = (0x91, 0xa0, 0xb4) # страницы за первой
SPINE = (0x15, 0x70, 0xd8) # синий корешок
GLASS = (0x2f, 0x91, 0xff) # лупа
def _round_corner(x: float, y: float, size: float, radius: float) -> bool:
"""@brief Точка внутри скруглённого квадрата 0..size?"""
cx = min(max(x, radius), size - radius)
cy = min(max(y, radius), size - radius)
return (x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2
def _pixel(u: float, v: float) -> tuple[int, int, int] | None:
"""@brief Цвет точки в относительных координатах 0..1 (None — прозрачно)."""
if not _round_corner(u, v, 1.0, 0.22):
return None
colour = BG
# Три страницы уступом: задние приглушены.
for shift, page in ((0.10, PAGE_DIM), (0.05, PAGE_DIM), (0.0, PAGE)):
left, right = 0.22 + shift, 0.72 + shift
top, bottom = 0.16 + shift, 0.80 + shift
if left <= u <= right and top <= v <= bottom:
colour = page
if u <= left + 0.06 and page is PAGE:
colour = SPINE # корешок у передней страницы
# Лупа: кольцо и ручка.
cx, cy, r = 0.66, 0.64, 0.19
d = ((u - cx) ** 2 + (v - cy) ** 2) ** 0.5
if r - 0.055 <= d <= r:
colour = GLASS
if 0.0 <= u - cx - 0.10 <= 0.055 and 0.0 <= v - cy - 0.10 <= 0.20:
if abs((u - cx - 0.135) - (v - cy - 0.135)) < 0.035:
colour = GLASS
return colour
# --------------------------------------------------------------------------
# Windows: .ico
# --------------------------------------------------------------------------
def _bgra_bottom_up(size: int) -> bytes:
"""@brief Растр BGRA снизу вверх — так его хранит формат ICO."""
rows = []
for y in range(size - 1, -1, -1):
row = bytearray()
for x in range(size):
px = _pixel((x + 0.5) / size, (y + 0.5) / size)
if px:
r, g, b = px
row += bytes((b, g, r, 255))
else:
row += TRANSPARENT
rows.append(bytes(row))
return b"".join(rows)
def _ico_entry(size: int) -> bytes:
"""@brief Одно изображение в формате BMP-в-ICO вместе с пустой маской."""
header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0,
size * size * 4, 0, 0, 0, 0)
mask_row = ((size + 31) // 32) * 4
return header + _bgra_bottom_up(size) + bytes(mask_row * size)
def build_ico(target: Path = TARGET_ICO) -> Path:
"""@brief Пишет многоразмерный .ico и возвращает путь к нему."""
images = [_ico_entry(size) for size in SIZES]
offset = 6 + 16 * len(images)
out = bytearray(struct.pack("<HHH", 0, 1, len(images)))
for size, data in zip(SIZES, images):
out += struct.pack("<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32,
len(data), offset)
offset += len(data)
for data in images:
out += data
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(bytes(out))
return target
# --------------------------------------------------------------------------
# macOS: .icns
# --------------------------------------------------------------------------
def _rgba_top_down(size: int) -> bytes:
"""@brief Растр RGBA сверху вниз, каждая строка с байтом фильтра PNG."""
rows = []
for y in range(size):
row = bytearray(FILTER_NONE)
for x in range(size):
px = _pixel((x + 0.5) / size, (y + 0.5) / size)
if px:
r, g, b = px
row += bytes((r, g, b, 255))
else:
row += TRANSPARENT
rows.append(bytes(row))
return b"".join(rows)
def _png(size: int) -> bytes:
"""@brief PNG без внешних библиотек: zlib и CRC из стандартной поставки."""
def chunk(tag: bytes, data: bytes) -> bytes:
body = tag + data
return (struct.pack(">I", len(data)) + body
+ struct.pack(">I", zlib.crc32(body) & 0xFFFFFFFF))
# Ширина, высота, 8 бит на канал, тип 6 (RGBA), без чересстрочности.
header = struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0)
return (PNG_MAGIC
+ chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(_rgba_top_down(size), 9))
+ chunk(b"IEND", b""))
def build_icns(target: Path = TARGET_ICNS) -> Path:
"""@brief Пишет .icns из PNG-изображений нескольких размеров."""
parts = []
for tag, size in ICNS_TYPES:
data = _png(size)
parts.append(tag + struct.pack(">I", len(data) + 8) + data)
body = b"".join(parts)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(b"icns" + struct.pack(">I", len(body) + 8) + body)
return target
if __name__ == "__main__":
print(build_ico())
print(build_icns())