diff --git a/README.md b/README.md index 037f9b4..d33e8d6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。 -当前版本 **0.5.0**。 +当前版本 **0.5.1**。 ## 界面 @@ -140,7 +140,9 @@ make -C build install INSTALL_ROOT=/tmp/pkg # 打包用,装到 /tmp/pkg/usr/lo ## 图标 -`logo.png` 是源文件,`packaging/` 里是从它生成的各平台图标: +`logo.png` 是源文件,`packaging/generate_icons.py` 从它生成各平台图标。 +源图是**黑色字标 + 透明背景**,直接做成图标放进深色任务栏会“消失”, +所以脚本会给它垫一块白色圆角底板,并输出多个尺寸(16–256): | 文件 | 平台 | | --- | --- | @@ -149,6 +151,7 @@ make -C build install INSTALL_ROOT=/tmp/pkg # 打包用,装到 /tmp/pkg/usr/lo | `voiletmark.png` | Linux 桌面图标(hicolor 256×256) | 运行时的窗口图标直接嵌在资源里(`resources.qrc`),所以不安装也有图标。 +改了 `logo.png` 之后跑一次 `python3 packaging/generate_icons.py` 重新生成即可。 ## 命令行 diff --git a/VoiletMark.pro b/VoiletMark.pro index 88188fd..0740806 100644 --- a/VoiletMark.pro +++ b/VoiletMark.pro @@ -1,6 +1,6 @@ TEMPLATE = app TARGET = VoiletMark -VERSION = 0.5.0 +VERSION = 0.5.1 QT += core gui widgets printsupport diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png index d7a2881..fb6c8ab 100644 Binary files a/docs/screenshot-dark.png and b/docs/screenshot-dark.png differ diff --git a/docs/screenshot-export.png b/docs/screenshot-export.png index 1e3c4c0..12331f5 100644 Binary files a/docs/screenshot-export.png and b/docs/screenshot-export.png differ diff --git a/docs/screenshot-light.png b/docs/screenshot-light.png index a5450f7..b13cf1e 100644 Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ diff --git a/docs/screenshot-source.png b/docs/screenshot-source.png index e740372..7185454 100644 Binary files a/docs/screenshot-source.png and b/docs/screenshot-source.png differ diff --git a/packaging/generate_icons.py b/packaging/generate_icons.py new file mode 100644 index 0000000..7a81347 --- /dev/null +++ b/packaging/generate_icons.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""从 logo.png 生成各平台图标。 + +logo.png 是黑色字标 + 透明背景:放在深色 Windows 任务栏里会“看不见”。 +所以先给它加一块白色圆角底板,再导出 ico / icns / png。 + +用法:python3 packaging/generate_icons.py +""" + +import os +import struct +import subprocess + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SOURCE = os.path.join(ROOT, "logo.png") +OUT_DIR = os.path.join(ROOT, "packaging") + +MASTER = 256 +SUPERSAMPLE = 2 +PLATE_RADIUS = 52 +GLYPH_RATIO = 0.66 +PLATE_COLOR = (255, 255, 255) +BORDER_COLOR = (222, 226, 230) + +SIZES = (16, 24, 32, 48, 64, 128, 256) + + +def run(args, **kwargs): + return subprocess.run(args, check=True, capture_output=True, **kwargs) + + +def load_rgba(path, size): + return run(["ffmpeg", "-v", "error", "-i", path, "-vf", + f"scale={size}:{size}:flags=lanczos", "-pix_fmt", "rgba", + "-f", "rawvideo", "-"]).stdout + + +def save_png(path, rgba, size): + run(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgba", + "-s", f"{size}x{size}", "-i", "-", path], input=rgba) + + +def rounded_mask(size, radius): + """圆角矩形覆盖率,左上角为 (0,0)。""" + mask = bytearray(size * size) + r = radius + for y in range(size): + for x in range(size): + dx = min(x, size - 1 - x) + dy = min(y, size - 1 - y) + if dx >= r or dy >= r: + mask[y * size + x] = 255 + continue + ddx = r - dx + ddy = r - dy + dist = (ddx * ddx + ddy * ddy) ** 0.5 + if dist <= r: + mask[y * size + x] = 255 + return mask + + +def compose_master(): + size = MASTER * SUPERSAMPLE + radius = PLATE_RADIUS * SUPERSAMPLE + border = 3 * SUPERSAMPLE + glyph_size = int(size * GLYPH_RATIO) + offset = (size - glyph_size) // 2 + + plate = rounded_mask(size, radius) + glyph = load_rgba(SOURCE, glyph_size) + + canvas = bytearray(size * size * 4) + for y in range(size): + for x in range(size): + idx = (y * size + x) * 4 + if not plate[y * size + x]: + continue + dx = min(x, size - 1 - x) + dy = min(y, size - 1 - y) + color = BORDER_COLOR if (dx < border or dy < border) else PLATE_COLOR + canvas[idx:idx + 4] = bytes((color[0], color[1], color[2], 255)) + + for gy in range(glyph_size): + for gx in range(glyph_size): + src = (gy * glyph_size + gx) * 4 + alpha = glyph[src + 3] / 255.0 + if alpha <= 0.0: + continue + dst = ((gy + offset) * size + (gx + offset)) * 4 + for channel in range(3): + canvas[dst + channel] = int( + glyph[src + channel] * alpha + canvas[dst + channel] * (1.0 - alpha)) + + # 缩小回 256,顺便得到平滑的边缘 + return run(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgba", + "-s", f"{size}x{size}", "-i", "-", "-vf", + f"scale={MASTER}:{MASTER}:flags=lanczos", "-frames:v", "1", + "-f", "rawvideo", "-"], input=bytes(canvas)).stdout + + +def bmp_entry(rgba, size): + header = struct.pack("> (x % 8) + mask += bits + return header + b"".join(rows) + bytes(mask) + + +def write_ico(path, images): + header = struct.pack("= 256 else size + entries += struct.pack("I", 8 + len(data)) + data + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + + master = compose_master() + master_png = os.path.join(OUT_DIR, "voiletmark.png") + save_png(master_png, master, MASTER) + + pngs = {} + master_bytes = open(master_png, "rb").read() + for size in SIZES: + if size == MASTER: + pngs[size] = master_bytes + continue + temp = os.path.join(OUT_DIR, f"_tmp_{size}.png") + run(["ffmpeg", "-y", "-v", "error", "-i", master_png, "-vf", + f"scale={size}:{size}:flags=lanczos", "-frames:v", "1", temp]) + pngs[size] = open(temp, "rb").read() + os.remove(temp) + + images = [] + for size in SIZES: + if size == MASTER: + images.append((size, pngs[size])) + else: + images.append((size, bmp_entry(load_rgba(master_png, size), size))) + write_ico(os.path.join(OUT_DIR, "voiletmark.ico"), images) + + body = chunk(b"ic07", pngs[128]) + chunk(b"ic08", pngs[256]) + with open(os.path.join(OUT_DIR, "voiletmark.icns"), "wb") as handle: + handle.write(b"icns" + struct.pack(">I", 8 + len(body)) + body) + + print("icons written to", OUT_DIR) + + +if __name__ == "__main__": + main() diff --git a/packaging/voiletmark.icns b/packaging/voiletmark.icns index 619fe5a..8bbd557 100644 Binary files a/packaging/voiletmark.icns and b/packaging/voiletmark.icns differ diff --git a/packaging/voiletmark.ico b/packaging/voiletmark.ico index cb28243..364c4f5 100644 Binary files a/packaging/voiletmark.ico and b/packaging/voiletmark.ico differ diff --git a/packaging/voiletmark.png b/packaging/voiletmark.png index 2d36fe1..5f877a1 100644 Binary files a/packaging/voiletmark.png and b/packaging/voiletmark.png differ diff --git a/resources.qrc b/resources.qrc index 90517ff..86cefbc 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,5 +1,5 @@ - logo.png + packaging/voiletmark.png diff --git a/src/ExportDialog.cpp b/src/ExportDialog.cpp index 3772ac3..918c9d6 100644 --- a/src/ExportDialog.cpp +++ b/src/ExportDialog.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,10 @@ ExportDialog::ExportDialog(const QString &markdown, const QString &documentName, { setWindowTitle(tr("导出 PDF — %1") .arg(documentName.isEmpty() ? tr("未命名") : documentName)); - resize(900, 840); + + // 别让窗口比屏幕还高,否则底部的按钮会顶到屏幕外面去。 + const QRect available = screen() ? screen()->availableGeometry() : QRect(0, 0, 1280, 900); + resize(qMin(900, available.width() - 80), qMin(860, available.height() - 100)); QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(16, 16, 16, 12); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index af24b1e..53d82ce 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -68,7 +68,7 @@ const char kWelcomeText[] = R"VOILETMARK( - [x] 自动深浅色主题 - [x] Markdown 语法高亮 -- [ ] 实时预览(下一版) +- [x] 导出 PDF(带预览) ```cpp #include @@ -671,7 +671,7 @@ void MainWindow::showAbout() { QMessageBox box(this); box.setWindowTitle(tr("关于 VoiletMark")); - box.setIconPixmap(QPixmap(QStringLiteral(":/logo.png")) + box.setIconPixmap(QPixmap(QStringLiteral(":/packaging/voiletmark.png")) .scaled(72, 72, Qt::KeepAspectRatio, Qt::SmoothTransformation)); box.setTextFormat(Qt::RichText); box.setText(tr("

VoiletMark %1

" diff --git a/src/main.cpp b/src/main.cpp index 6858089..c80aa85 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,7 +16,7 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); - QApplication::setWindowIcon(QIcon(QStringLiteral(":/logo.png"))); + QApplication::setWindowIcon(QIcon(QStringLiteral(":/packaging/voiletmark.png"))); QGuiApplication::setDesktopFileName(QStringLiteral("voiletmark")); QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));