Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/macos-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build macOS Artifacts

on:
workflow_dispatch:
push:
branches:
- codex/macos-build
paths:
- ".github/workflows/macos-artifacts.yml"
- "app/**"
- "pdf2zh/**"
- "scripts/**"
- "app-macos.spec"
- "build-macos.sh"
- "requirements*.txt"

permissions:
contents: read

jobs:
build:
name: macOS (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: apple-silicon
runner: macos-15
- arch: intel
runner: macos-15-intel
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Build app and DMG
shell: bash
run: bash build-macos.sh

- uses: actions/upload-artifact@v6
with:
name: PDFTranslate-macos-${{ matrix.arch }}
path: dist/PDFTranslate-macos-${{ matrix.arch }}.dmg
if-no-files-found: error
retention-days: 7
66 changes: 62 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ name: Release
# right every time - which is exactly the kind of thing manual uploads get
# wrong once and then nobody notices for a month.
on:
workflow_dispatch:
push:
tags:
- "v*"
Expand All @@ -16,13 +17,14 @@ jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Check the tag matches APP_VERSION
if: startsWith(github.ref, 'refs/tags/v')
shell: pwsh
run: |
$tag = "${{ github.ref_name }}".TrimStart("v")
Expand All @@ -41,7 +43,63 @@ jobs:
- name: Build
run: .\build.ps1

- uses: softprops/action-gh-release@v2
- uses: actions/upload-artifact@v6
with:
files: dist/PDFTranslate-windows.zip
name: PDFTranslate-windows
path: dist/PDFTranslate-windows.zip
if-no-files-found: error

macos:
name: macOS (${{ matrix.arch }})
strategy:
fail-fast: false
matrix:
include:
- arch: apple-silicon
runner: macos-15
- arch: intel
runner: macos-15-intel
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v6

- uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Check the tag matches APP_VERSION
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
tag="${GITHUB_REF_NAME#v}"
version="$(sed -n 's/^APP_VERSION = "\([^"]*\)"$/\1/p' app/update.py)"
if [[ "$tag" != "$version" ]]; then
echo "Tag $tag does not match APP_VERSION $version in app/update.py" >&2
exit 1
fi
echo "Releasing $version"

- name: Build app and DMG
shell: bash
run: bash build-macos.sh

- uses: actions/upload-artifact@v6
with:
name: PDFTranslate-macos-${{ matrix.arch }}
path: dist/PDFTranslate-macos-${{ matrix.arch }}.dmg
if-no-files-found: error

publish:
if: startsWith(github.ref, 'refs/tags/v')
needs: [windows, macos]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v7
with:
path: release-assets
merge-multiple: true

- uses: softprops/action-gh-release@v3
with:
files: release-assets/*
generate_release_notes: true
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
<a href="https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-windows.zip">
<img src="https://img.shields.io/badge/TẢI_XUỐNG-Windows_x64-1f6feb?style=for-the-badge&logo=windows11&logoColor=white" alt="Tải PDF Translate cho Windows">
</a>
<a href="https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-macos-apple-silicon.dmg">
<img src="https://img.shields.io/badge/TẢI_XUỐNG-macOS_Apple_Silicon-111111?style=for-the-badge&logo=apple&logoColor=white" alt="Tải PDF Translate cho Mac Apple Silicon">
</a>
<a href="https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-macos-intel.dmg">
<img src="https://img.shields.io/badge/TẢI_XUỐNG-macOS_Intel-555555?style=for-the-badge&logo=apple&logoColor=white" alt="Tải PDF Translate cho Mac Intel">
</a>
</p>

<p align="center">
Expand All @@ -35,7 +41,7 @@

---

PDF Translate là ứng dụng desktop mã nguồn mở dành cho Windows. Công cụ phân tích bố cục từng trang, bảo vệ công thức và code, dịch phần văn xuôi rồi đặt nội dung trở lại đúng vị trí trong tài liệu gốc — không biến PDF của bạn thành một trang chữ trắng đơn giản.
PDF Translate là ứng dụng desktop mã nguồn mở dành cho Windows và macOS. Công cụ phân tích bố cục từng trang, bảo vệ công thức và code, dịch phần văn xuôi rồi đặt nội dung trở lại đúng vị trí trong tài liệu gốc — không biến PDF của bạn thành một trang chữ trắng đơn giản.

## Điểm nổi bật

Expand All @@ -49,16 +55,27 @@ PDF Translate là ứng dụng desktop mã nguồn mở dành cho Windows. Công

## Bắt đầu trong 1 phút

### Windows

1. **[Tải PDF Translate cho Windows](https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-windows.zip)** (`.zip`, khoảng 199 MB).
2. Giải nén toàn bộ file vừa tải.
3. Mở `PDFTranslate.exe`.

### macOS

1. Tải bản phù hợp: **[Apple Silicon](https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-macos-apple-silicon.dmg)** cho Mac M1/M2/M3/M4/M5 hoặc **[Intel](https://github.com/breslee1707/VI-Translate/releases/latest/download/PDFTranslate-macos-intel.dmg)** cho Mac Intel.
2. Mở file `.dmg`, kéo **PDF Translate** vào thư mục **Applications**.
3. Trong lần chạy đầu, bấm chuột phải vào ứng dụng → **Open** → **Open**.

> [!NOTE]
> Ứng dụng hiện chỉ hỗ trợ **Windows 64-bit**. Bản desktop không cần cài Python và không phải tải thêm model ở lần chạy đầu. Quá trình dịch bằng Google vẫn cần kết nối Internet.
> Bản macOS yêu cầu **macOS 14 Sonoma trở lên**. Tất cả bản desktop đều không cần cài Python và không phải tải thêm model ở lần chạy đầu. Quá trình dịch bằng Google vẫn cần kết nối Internet.

> [!WARNING]
> Windows SmartScreen có thể cảnh báo vì ứng dụng chưa được ký số. Chọn **More info** → **Run anyway** nếu bạn tải file từ trang Releases chính thức của repo này.

> [!WARNING]
> Bản macOS hiện dùng chữ ký ad-hoc, chưa được Apple notarize. Gatekeeper có thể chặn thao tác mở thông thường; hãy dùng cách bấm chuột phải → **Open** ở trên nếu bạn tải từ trang Releases chính thức.

Bạn cũng có thể mở [trang Releases](https://github.com/breslee1707/VI-Translate/releases/latest) để xem ghi chú thay đổi và các tệp của phiên bản mới nhất.

Ứng dụng cũng tự kiểm tra phiên bản mới mỗi lần mở. Có bản mới thì góc trên bên phải hiện dòng **● Có bản mới vX.Y.Z**, bấm vào là mở trang tải. Ứng dụng không tự tải và không tự cài đè — bạn vẫn tự giải nén như lần đầu. Máy không có mạng thì bỏ qua, không báo lỗi.
Expand All @@ -71,7 +88,7 @@ Chọn một trong ba cách:

- Kéo thả file PDF hoặc cả thư mục vào cửa sổ ứng dụng.
- Bấm **Chọn file** hoặc **Chọn thư mục**.
- Thả file trực tiếp lên `PDFTranslate.exe`.
- Thả file trực tiếp lên biểu tượng ứng dụng.

### 2. Chọn ngôn ngữ

Expand Down Expand Up @@ -174,6 +191,16 @@ python -m venv .venv

Gói phát hành được tạo tại `dist\PDFTranslate-windows.zip`.

### Build ứng dụng macOS

Chạy trên máy Mac dùng đúng kiến trúc cần phát hành:

```bash
bash build-macos.sh
```

Gói phát hành được tạo tại `dist/PDFTranslate-macos-apple-silicon.dmg` hoặc `dist/PDFTranslate-macos-intel.dmg`. Từ máy Windows, bạn có thể chạy thủ công workflow **Release** trên GitHub Actions để lấy cả hai DMG trong phần Artifacts; khi push tag `v*`, workflow tự đính kèm chúng vào GitHub Release.

## Giới hạn hiện tại

- **Chưa có OCR:** PDF scan chỉ chứa hình ảnh cần được OCR trước khi dịch.
Expand Down
114 changes: 114 additions & 0 deletions app-macos.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for the macOS desktop app.

Build on the target architecture. PyInstaller is not a cross-compiler, and the
large native dependencies make separate arm64 and x86_64 DMGs more reliable
than trying to assemble a universal2 bundle from thin wheels.
"""

import os
import runpy
from pathlib import Path

from PyInstaller.utils.hooks import collect_data_files

ROOT = Path(SPECPATH)
APP_VERSION = runpy.run_path(str(ROOT / "app" / "update.py"))["APP_VERSION"]
TARGET_ARCH = os.environ.get("MACOS_TARGET_ARCH") or None
SIGNING_IDENTITY = os.environ.get("MACOS_SIGNING_IDENTITY") or None
ICON = ROOT / "build" / "macos" / "PDFTranslate.icns"

if not ICON.is_file():
raise SystemExit("Missing macOS icon. Run ./build-macos.sh instead of PyInstaller directly.")

datas = []
for optional in ("app/fonts", "app/assets"):
directory = ROOT / optional
if not directory.is_dir():
continue
for item in sorted(directory.iterdir()):
if item.is_file() and item.suffix != ".optimized":
datas.append((str(item), optional))

datas += collect_data_files("customtkinter")
datas += collect_data_files("tkinterdnd2")
datas += collect_data_files("babeldoc")

hiddenimports = [
"peewee",
"pdf2zh.doclayout",
"pdf2zh.high_level",
"pdf2zh.converter",
"pdf2zh.translator",
]

analysis = Analysis(
[str(ROOT / "app" / "gui.py")],
pathex=[str(ROOT)],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
"matplotlib",
"PyQt5",
"PyQt6",
"PySide2",
"PySide6",
"IPython",
"pytest",
"scipy",
"pandas",
"onnxruntime.transformers",
"onnxruntime.tools",
"onnxruntime.quantization",
],
noarchive=False,
)

pyz = PYZ(analysis.pure)

exe = EXE(
pyz,
analysis.scripts,
[],
exclude_binaries=True,
name="PDFTranslate",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=TARGET_ARCH,
codesign_identity=SIGNING_IDENTITY,
entitlements_file=None,
)

collect = COLLECT(
exe,
analysis.binaries,
analysis.datas,
strip=False,
upx=False,
name="PDFTranslate",
)

app = BUNDLE(
collect,
name="PDFTranslate.app",
icon=str(ICON),
bundle_identifier="ai.huyg.pdftranslate",
version=APP_VERSION,
info_plist={
"CFBundleDisplayName": "PDF Translate",
"LSMinimumSystemVersion": "14.0",
"NSHighResolutionCapable": True,
},
target_arch=TARGET_ARCH,
codesign_identity=SIGNING_IDENTITY,
entitlements_file=None,
)
34 changes: 28 additions & 6 deletions app/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ctypes
import os
import queue
import subprocess
import sys
import threading
import tkinter
Expand Down Expand Up @@ -39,8 +40,12 @@
ASSET_DIRECTORY = APP_ROOT / "app" / "assets"
UI_FONT = "Be Vietnam Pro"
MONO_FONT = "JetBrains Mono"
FALLBACK_UI_FONT = "Segoe UI"
FALLBACK_MONO_FONT = "Consolas"
if sys.platform == "darwin":
FALLBACK_UI_FONT = "Helvetica Neue"
FALLBACK_MONO_FONT = "Menlo"
else:
FALLBACK_UI_FONT = "Segoe UI"
FALLBACK_MONO_FONT = "Consolas"

# One 8px rhythm for the whole window, so nothing is spaced by feel.
PAD, GAP, EDGE = 8, 16, 24
Expand Down Expand Up @@ -464,7 +469,16 @@ def _open(target: Path | None) -> None:
if target is None or not Path(target).exists():
return
try:
os.startfile(target) # noqa: S606 - Windows shell open, the app is Windows only
if sys.platform == "win32":
os.startfile(target) # type: ignore[attr-defined] # noqa: S606
else:
command = "open" if sys.platform == "darwin" else "xdg-open"
subprocess.Popen( # noqa: S603 - fixed OS command, no shell
[command, str(target)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except OSError:
pass # nothing useful to do if the shell refuses

Expand Down Expand Up @@ -702,9 +716,17 @@ def main() -> None:
ctk.set_appearance_mode("system")
ctk.set_default_color_theme("blue")
app = App()
# Windows passes anything dropped on the executable icon as arguments.
if sys.argv[1:]:
app._add([Path(argument) for argument in sys.argv[1:]])
arguments = [argument for argument in sys.argv[1:] if argument != "--smoke-test"]
if "--smoke-test" in sys.argv[1:]:
# CI uses this to prove the frozen executable can load Tk, TkDND and all
# native libraries on the Mac architecture that produced the bundle.
app.withdraw()
app.update_idletasks()
app.destroy()
return
# Desktop shells can pass files dropped on the executable icon as arguments.
if arguments:
app._add([Path(argument) for argument in arguments])
app.mainloop()


Expand Down
Loading
Loading