feat: 新增 OCRmyPDF 轉換引擎 (v0.1.14)

## 新功能
- OCRmyPDF 轉換引擎:將掃描版 PDF 轉換為可搜尋 PDF
  - 支援 7 種語言:en, zh-TW, zh, ja, ko, de, fr
  - 與 PDFMathTranslate 風格一致的 UI 格式 (pdf-<lang>)
  - 自動偵測頁面方向並旋轉
  - 自動校正傾斜
  - 跳過已有文字層的頁面
  - 詳細的 5 階段處理進度輸出

## 建置
- Dockerfile:安裝 ocrmypdf 與 Tesseract OCR 語言包

## 文件
- 更新 OCR 功能文件
- 文件目錄結構改為中文名稱

## 測試
- 修復 BabelDOC 和 PDFMathTranslate 測試的 OCR mock
- 所有 345 個測試通過
This commit is contained in:
Your Name 2026-01-23 16:28:33 +08:00
parent f24eec070c
commit a06df23b1d
53 changed files with 1427 additions and 675 deletions

View file

@ -1,5 +1,29 @@
# Changelog
## [0.1.14](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.14) (2026-01-23)
OCR 功能強化版本,新增 OCRmyPDF 轉換引擎。
### ✨ Features
- **OCRmyPDF 轉換引擎**:新增獨立的 OCR 轉換引擎,支援將掃描版 PDF 轉換為可搜尋 PDF
- 支援 7 種語言:英文、繁體中文、簡體中文、日文、韓文、德文、法文
- 與 PDFMathTranslate 風格一致的 UI 格式(`pdf-en``pdf-zh-TW` 等)
- 自動偵測頁面方向並旋轉
- 自動校正傾斜
- 跳過已有文字層的頁面
- 詳細的5階段處理進度輸出
### 📦 Build
- **Dockerfile**:安裝 ocrmypdf 與 Tesseract OCR 語言包(階段 9/11
### 📚 Documentation
- 更新 OCR 功能文件,說明 OCRmyPDF 轉換引擎用法
---
## [0.1.13](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.13) (2026-01-23)
工作流程優化與測試修復版本。

View file

@ -116,15 +116,24 @@ RUN echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries \
&& echo 'DPkg::Lock::Timeout "120";' >> /etc/apt/apt.conf.d/80-retries
# 階段 1基礎系統工具
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 1/11安裝基礎系統工具" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
locales \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 1/11 完成:基礎系統工具已安裝"
# 階段 2核心轉換工具小型
# 注意dasel 和 resvg 在 bookworm 中不存在,後續用二進位檔案安裝
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 2/11安裝核心轉換工具" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
assimp-utils \
dcraw \
dvisvgm \
@ -133,64 +142,95 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
mupdf-tools \
poppler-utils \
potrace \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 2/11 完成:核心轉換工具已安裝"
# 階段 2.1:安裝 dasel從 GitHub 下載二進位檔案)
RUN ARCH=$(uname -m) && \
RUN echo "" && \
echo " 🔧 階段 2.1:安裝 dasel..." && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
DASEL_ARCH="linux_arm64"; \
else \
DASEL_ARCH="linux_amd64"; \
fi && \
curl -sSLf "https://github.com/TomWright/dasel/releases/download/v2.8.1/dasel_${DASEL_ARCH}" -o /usr/local/bin/dasel && \
chmod +x /usr/local/bin/dasel
chmod +x /usr/local/bin/dasel && \
echo " ✅ dasel 安裝完成"
# 階段 2.2:安裝 resvg從 GitHub 下載二進位檔案)
# 注意resvg 官方只提供 x86_64 版本ARM64 需從源碼編譯或跳過
RUN ARCH=$(uname -m) && \
RUN echo "" && \
echo " 🔧 階段 2.2:安裝 resvg..." && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
echo "⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝(可改用 ImageMagick 或 Inkscape 替代)"; \
echo " ⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝"; \
else \
curl -sSLf "https://github.com/linebender/resvg/releases/download/v0.44.0/resvg-linux-x86_64.tar.gz" -o /tmp/resvg.tar.gz && \
tar -xzf /tmp/resvg.tar.gz -C /tmp/ && \
mv /tmp/resvg /usr/local/bin/resvg && \
chmod +x /usr/local/bin/resvg && \
rm -rf /tmp/resvg.tar.gz; \
rm -rf /tmp/resvg.tar.gz && \
echo " ✅ resvg 安裝完成"; \
fi
# 階段 3影音處理工具
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 3/11安裝影音處理工具" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
ffmpeg \
libavcodec-extra \
libva2 \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 3/11 完成影音處理工具已安裝ffmpeg"
# 階段 4圖像處理工具
# 注意bookworm 使用 imagemagick版本 6trixie 才有 imagemagick-7
# 注意Inkscape 需要 xvfb 在無 DISPLAY 環境下執行某些操作(如 PNG 轉 SVG
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 4/11安裝圖像處理工具" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
imagemagick \
inkscape \
libheif-examples \
libjxl-tools \
libvips-tools \
xvfb \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 4/11 完成圖像處理工具已安裝ImageMagick, Inkscape, VIPS"
# 階段 5文件處理工具
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 5/11安裝文件處理工具" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
calibre \
libemail-outlook-message-perl \
pandoc \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 5/11 完成文件處理工具已安裝Calibre, Pandoc"
# 階段 6LibreOffice最大的套件單獨安裝
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 6/11安裝 LibreOffice較大需要數分鐘" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
libreoffice \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 6/11 完成LibreOffice 已安裝"
# 階段 7TexLive 基礎
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 7/11安裝 TexLive 基礎(較大,需要數分鐘)" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
texlive-base \
texlive-latex-base \
texlive-latex-recommended \
@ -198,19 +238,29 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
texlive-xetex \
latexmk \
lmodern \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 7/11 完成TexLive 基礎已安裝"
# 階段 8TexLive 語言包
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 8/11安裝 TexLive 語言包CJK + 歐語)" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
texlive-lang-cjk \
texlive-lang-german \
texlive-lang-french \
texlive-lang-arabic \
texlive-lang-other \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 8/11 完成TexLive 語言包已安裝"
# 階段 9OCR 支援
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 9/11安裝 OCR 支援Tesseract + ocrmypdf" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-eng \
tesseract-ocr-chi-tra \
@ -219,25 +269,41 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
tesseract-ocr-kor \
tesseract-ocr-deu \
tesseract-ocr-fra \
&& rm -rf /var/lib/apt/lists/*
ocrmypdf \
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 9/11 完成OCR 支援已安裝7 種語言)"
# 階段 10字型
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 10/11安裝字型Noto CJK + Liberation" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
fonts-noto-cjk \
fonts-noto-core \
fonts-noto-color-emoji \
fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 10/11 完成:字型已安裝"
# 階段 11Python 依賴
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
RUN echo "" && \
echo "========================================" && \
echo "📦 階段 11/11安裝 Python 依賴" && \
echo "========================================" && \
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
python3-numpy \
python3-tinycss2 \
python3-opencv \
pipx \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
echo "✅ 階段 11/11 完成Python 依賴已安裝" && \
echo "" && \
echo "========================================" && \
echo "✅ 所有 APT 套件安裝完成!" && \
echo "========================================"
# ==============================================================================
# 🔥 階段 12-UNIFIEDPython 工具安裝 + 模型下載(單一 RUN 原則)

160
LICENSE
View file

@ -13,7 +13,7 @@ cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
@ -24,10 +24,10 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
which gives you legal permission to copy, distribute and/or modify the
software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
@ -48,10 +48,10 @@ a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
published by Affero, was designed to accomplish similar goals. This
is a different license, not a version of the Affero GPL, but Affero
has released a new version of the Affero GPL which permits relicensing
under this license.
The precise terms and conditions for copying, distribution and
modification follow.
@ -60,19 +60,20 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU Affero General Public
License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"Copyright" also means copyright-like laws that apply to other kinds
of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
To "modify" a work means to copy from or adapt all or part of the
work in a fashion requiring copyright permission, other than the making
of an exact copy. The resulting work is called a "modified version"
of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
@ -317,8 +318,8 @@ been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
the User Product in which it has been modified or installed. Access to
a network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
@ -347,8 +348,8 @@ additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
add to a covered work, you may (if authorized by the copyright holders
of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
@ -531,53 +532,44 @@ otherwise be available to you under applicable patent law.
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
License and any other pertinent obligations, then as a consequence you
may not convey it at all. For example, if you agree to terms that
obligate you to collect a royalty for further conveying from those to
whom you convey the Program, the only way you could satisfy both those
terms and this License would be to refrain entirely from conveying the
Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
The Free Software Foundation may publish revised and/or new versions
of the GNU Affero General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
Program specifies that a certain numbered version of the GNU Affero
General Public License "or any later version" applies to it, you have
the option of following the terms and conditions either of that
numbered version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number
of the GNU Affero General Public License, you may choose any version
ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
versions of the GNU Affero General Public License can be used, that
proxy's public statement of acceptance of a version permanently
authorizes you to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
@ -591,21 +583,21 @@ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
@ -617,45 +609,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View file

@ -6,18 +6,20 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/convertx/convertx-cn?style=flat&logo=docker)](https://hub.docker.com/r/convertx/convertx-cn)
[![GitHub Release](https://img.shields.io/github/v/release/pi-docket/ConvertX-CN)](https://github.com/pi-docket/ConvertX-CN/releases)
[![License AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE)
---
## 為什麼選擇 ConvertX-CN
| 特色 | 說明 |
| ----------------- | ------------------------------------ |
| ----------------- | --------------------------------------- |
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
| 🈶 **中文優化** | 內建中日韓字型與 OCR告別亂碼 |
| 🌐 **65 種語言** | 跨國團隊無障礙使用 |
| 📊 **PDF 翻譯** | 數學公式完整保留PDFMathTranslate |
| 📊 **PDF 翻譯** | PDFMathTranslate + BabelDOC 雙引擎 |
| 📄 **PDF 轉 MD** | MinerU 智能擷取(保留表格、公式、圖片) |
---
@ -26,25 +28,42 @@
完整文件請參閱 **[文件中心](docs/README.md)**
| 分類 | 連結 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
| 🚀 快速入門 | [概覽](docs/getting-started/overview.md) · [快速開始](docs/getting-started/quick-start.md) · [FAQ](docs/getting-started/faq.md) |
| 🐳 部署指南 | [Docker](docs/deployment/docker.md) · [反向代理](docs/deployment/reverse-proxy.md) |
| ⚙️ 配置設定 | [環境變數](docs/configuration/environment-variables.md) · [安全性](docs/configuration/security.md) |
| 🔌 功能說明 | [轉換器](docs/features/converters.md) · [OCR](docs/features/ocr.md) · [翻譯](docs/features/translation.md) |
| 🔗 API | [API 總覽](docs/api/overview.md) · [端點說明](docs/api/endpoints.md) |
| 👩‍💻 開發 | [專案結構](docs/development/project-structure.md) · [貢獻指南](docs/development/contribution.md) |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| 🚀 快速入門 | [概覽](docs/快速入門/概覽.md) · [快速開始](docs/快速入門/快速開始.md) · [FAQ](docs/快速入門/常見問題.md) |
| 🐳 部署指南 | [Docker](docs/部署指南/Docker.md) · [反向代理](docs/部署指南/反向代理.md) |
| ⚙️ 配置設定 | [環境變數](docs/配置設定/環境變數.md) · [安全性](docs/配置設定/安全性.md) |
| 🔌 功能說明 | [轉換器](docs/功能說明/轉換器.md) · [OCR](docs/功能說明/OCR.md) · [翻譯](docs/功能說明/翻譯.md) |
| 🔗 API | [API 總覽](docs/API/總覽.md) · [端點說明](docs/API/端點.md) |
| 👩‍💻 開發 | [專案結構](docs/開發指南/專案結構.md) · [貢獻指南](docs/開發指南/貢獻指南.md) |
---
## 🚀 快速開始
### Docker Compose推薦
### Docker Run
```bash
# 1. 建立專案資料夾
mkdir -p ~/convertx-cn && cd ~/convertx-cn
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
docker run -d \
--name convertx-cn \
--restart unless-stopped \
-p 3000:3000 \
-v ./data:/app/data \
-e TZ=Asia/Taipei \
-e JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0 \
convertx/convertx-cn:latest
```
# 2. 建立 docker-compose.yml
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
開啟瀏覽器:`http://localhost:3000`
### Docker Compose推薦
> 💡 以下命令會自動建立 `~/convertx-cn/data` 資料夾、產生 `docker-compose.yml` 並啟動服務
```bash
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
cat > docker-compose.yml << 'EOF'
services:
convertx:
@ -57,31 +76,16 @@ services:
- ./data:/app/data
environment:
- TZ=Asia/Taipei
- JWT_SECRET=請更換為長且隨機的字串至少32字元
- JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0
EOF
# 3. 啟動
docker compose up -d
```
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
開啟瀏覽器:`http://localhost:3000`
> 📖 詳細說明請參閱 [快速開始](docs/getting-started/quick-start.md)
### Docker Run
```bash
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn
docker run -d \
--name convertx-cn \
--restart unless-stopped \
-p 3000:3000 \
-v ./data:/app/data \
-e TZ=Asia/Taipei \
-e JWT_SECRET=請更換為長且隨機的字串 \
convertx/convertx-cn:latest
```
> 📖 詳細說明請參閱 [快速開始](docs/快速入門/快速開始.md)
---
@ -94,7 +98,7 @@ docker run -d \
| 帳號 | admin@example.com |
| 密碼 | admin |
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案,會定期清理資料。
---
@ -106,14 +110,14 @@ docker run -d \
| 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 |
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
更多問題 → [FAQ](docs/getting-started/faq.md)
更多問題 → [FAQ](docs/快速入門/常見問題.md)
---
## 📦 支援格式
| 轉換器 | 用途 | 格式數 |
| ---------------- | -------- | ------ |
| ---------------- | --------------- | ------ |
| FFmpeg | 影音 | 400+ |
| ImageMagick | 圖片 | 200+ |
| LibreOffice | 文件 | 60+ |
@ -121,8 +125,10 @@ docker run -d \
| Calibre | 電子書 | 40+ |
| Inkscape | 向量圖 | 20+ |
| PDFMathTranslate | PDF 翻譯 | 15+ |
| BabelDOC | PDF 翻譯/轉換 | 15+ |
| MinerU | PDF 轉 Markdown | 10+ |
完整列表 → [轉換器文件](docs/features/converters.md)
完整列表 → [轉換器文件](docs/功能說明/轉換器.md)
---
@ -144,4 +150,6 @@ docker compose up -d
## 📄 License
[MIT](LICENSE) | 基於 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
This project is licensed under the **[GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE)**.
Based on [C4illin/ConvertX](https://github.com/C4illin/ConvertX).

View file

@ -6,4 +6,4 @@ Only the latest release is supported
## Reporting a Vulnerability
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/pi-docket/ConvertX-CN/security/advisories/new) tab.

View file

@ -123,6 +123,6 @@ curl -X POST \
## 相關文件
- [API 端點詳細說明](endpoints.md)
- [API 端點詳細說明](端點.md)
- [API 規格文件](../../api-server/docs/API_SPEC.md)
- [架構說明](../../api-server/docs/ARCHITECTURE.md)

View file

@ -8,9 +8,9 @@
剛開始使用?從這裡開始:
1. **[概覽](getting-started/overview.md)** — 了解 ConvertX-CN 是什麼
2. **[快速開始](getting-started/quick-start.md)** — 5 分鐘內完成部署
3. **[常見問題](getting-started/faq.md)** — 解決常見問題
1. **[概覽](快速入門/概覽.md)** — 了解 ConvertX-CN 是什麼
2. **[快速開始](快速入門/快速開始.md)** — 5 分鐘內完成部署
3. **[常見問題](快速入門/常見問題.md)** — 解決常見問題
---
@ -19,13 +19,13 @@
適合一般使用者:
| 文件 | 說明 |
| ------------------------------------------ | -------------------- |
| [快速開始](getting-started/quick-start.md) | 最快部署方式 |
| [支援的轉換器](features/converters.md) | 所有可用的轉換格式 |
| [OCR 功能](features/ocr.md) | 光學字元辨識 |
| [翻譯功能](features/translation.md) | PDF 翻譯(保留公式) |
| [多語言介面](features/i18n.md) | 切換介面語言 |
| [常見問題](getting-started/faq.md) | FAQ |
| ---------------------------------- | -------------------- |
| [快速開始](快速入門/快速開始.md) | 最快部署方式 |
| [支援的轉換器](功能說明/轉換器.md) | 所有可用的轉換格式 |
| [OCR 功能](功能說明/OCR.md) | 光學字元辨識 |
| [翻譯功能](功能說明/翻譯.md) | PDF 翻譯(保留公式) |
| [多語言介面](功能說明/多語言.md) | 切換介面語言 |
| [常見問題](快速入門/常見問題.md) | FAQ |
---
@ -36,18 +36,18 @@
### 部署
| 文件 | 說明 |
| --------------------------------------- | --------------------------- |
| [Docker 部署](deployment/docker.md) | Docker Run & Docker Compose |
| [反向代理](deployment/reverse-proxy.md) | Nginx / Traefik / Caddy |
| [範例配置](samples/README.md) | 可直接使用的配置檔 |
| --------------------------------- | --------------------------- |
| [Docker 部署](部署指南/Docker.md) | Docker Run & Docker Compose |
| [反向代理](部署指南/反向代理.md) | Nginx / Traefik / Caddy |
| [範例配置](範例/README.md) | 可直接使用的配置檔 |
### 配置
| 文件 | 說明 |
| -------------------------------------------------- | ------------------ |
| [環境變數](configuration/environment-variables.md) | 所有可用設定 |
| [安全性設定](configuration/security.md) | HTTPS、認證、防護 |
| [清理與限制](configuration/limits-and-cleanup.md) | 自動清理、資源限制 |
| ------------------------------------ | ------------------ |
| [環境變數](配置設定/環境變數.md) | 所有可用設定 |
| [安全性設定](配置設定/安全性.md) | HTTPS、認證、防護 |
| [清理與限制](配置設定/清理與限制.md) | 自動清理、資源限制 |
---
@ -58,25 +58,25 @@
### 開發
| 文件 | 說明 |
| -------------------------------------------- | -------------- |
| [專案結構](development/project-structure.md) | 程式碼結構說明 |
| [本地開發](development/local-development.md) | 開發環境設定 |
| [貢獻指南](development/contribution.md) | 如何參與專案 |
| -------------------------------- | -------------- |
| [專案結構](開發指南/專案結構.md) | 程式碼結構說明 |
| [本地開發](開發指南/本地開發.md) | 開發環境設定 |
| [貢獻指南](開發指南/貢獻指南.md) | 如何參與專案 |
### API
| 文件 | 說明 |
| ---------------------------- | ------------------ |
| [API 總覽](api/overview.md) | REST & GraphQL API |
| [API 端點](api/endpoints.md) | 詳細端點說明 |
| ----------------------- | ------------------ |
| [API 總覽](API/總覽.md) | REST & GraphQL API |
| [API 端點](API/端點.md) | 詳細端點說明 |
### 測試
| 文件 | 說明 |
| ------------------------------------ | -------------- |
| [測試策略](testing/test-strategy.md) | 測試類型與方法 |
| [CI/CD](testing/ci-cd.md) | 持續整合設定 |
| [E2E 測試](testing/e2e-tests.md) | 端對端測試 |
| ---------------------------- | -------------- |
| [測試策略](測試/測試策略.md) | 測試類型與方法 |
| [CI/CD](測試/CI-CD.md) | 持續整合設定 |
| [E2E 測試](測試/E2E測試.md) | 端對端測試 |
---
@ -85,38 +85,38 @@
```
docs/
├── README.md ← 您在這裡
├── getting-started/ # 快速入門
│ ├── overview.md # 概覽
│ ├── quick-start.md # 快速開始
│ └── faq.md # 常見問題
├── deployment/ # 部署指南
│ ├── docker.md # Docker 部署
│ └── reverse-proxy.md # 反向代理
├── configuration/ # 配置設定
│ ├── environment-variables.md # 環境變數
│ ├── security.md # 安全性
│ └── limits-and-cleanup.md # 清理與限制
├── features/ # 功能說明
│ ├── converters.md # 轉換器
│ ├── translation.md # 翻譯
│ ├── ocr.md # OCR
│ └── i18n.md # 多語言
├── api/ # API 文件
│ ├── overview.md # API 總覽
│ └── endpoints.md # API 端點
├── testing/ # 測試
│ ├── test-strategy.md # 測試策略
│ ├── ci-cd.md # CI/CD
│ └── e2e-tests.md # E2E 測試
├── development/ # 開發指南
│ ├── project-structure.md # 專案結構
│ ├── local-development.md # 本地開發
│ └── contribution.md # 貢獻指南
└── samples/ # 範例檔案
├── compose.minimal.yml # 最小配置
├── compose.production.yml # 生產環境
├── compose.with-traefik.yml # Traefik 整合
└── nginx.example.conf # Nginx 設定
├── 快速入門/
│ ├── 概覽.md
│ ├── 快速開始.md
│ └── 常見問題.md
├── 部署指南/
│ ├── Docker.md
│ └── 反向代理.md
├── 配置設定/
│ ├── 環境變數.md
│ ├── 安全性.md
│ └── 清理與限制.md
├── 功能說明/
│ ├── 轉換器.md
│ ├── 翻譯.md
│ ├── OCR.md
│ └── 多語言.md
├── API/
│ ├── 總覽.md
│ └── 端點.md
├── 測試/
│ ├── 測試策略.md
│ ├── CI-CD.md
│ └── E2E測試.md
├── 開發指南/
│ ├── 專案結構.md
│ ├── 本地開發.md
│ └── 貢獻指南.md
└── 範例/
├── compose.minimal.yml
├── compose.production.yml
├── compose.with-traefik.yml
└── nginx.example.conf
```
---
@ -135,23 +135,23 @@ docs/
### 我是新手
1. [概覽](getting-started/overview.md)
2. [快速開始](getting-started/quick-start.md)
3. [支援的轉換器](features/converters.md)
1. [概覽](快速入門/概覽.md)
2. [快速開始](快速入門/快速開始.md)
3. [支援的轉換器](功能說明/轉換器.md)
### 我要部署到生產環境
1. [Docker 部署](deployment/docker.md)
2. [反向代理](deployment/reverse-proxy.md)
3. [安全性設定](configuration/security.md)
4. [環境變數](configuration/environment-variables.md)
1. [Docker 部署](部署指南/Docker.md)
2. [反向代理](部署指南/反向代理.md)
3. [安全性設定](配置設定/安全性.md)
4. [環境變數](配置設定/環境變數.md)
### 我想參與開發
1. [專案結構](development/project-structure.md)
2. [本地開發](development/local-development.md)
3. [貢獻指南](development/contribution.md)
4. [測試策略](testing/test-strategy.md)
1. [專案結構](開發指南/專案結構.md)
2. [本地開發](開發指南/本地開發.md)
3. [貢獻指南](開發指南/貢獻指南.md)
4. [測試策略](測試/測試策略.md)
---

View file

@ -4,8 +4,8 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🐳 [Docker 部署(含硬體加速)](deployment/docker.md)
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
> - 🐳 [Docker 部署(含硬體加速)](部署指南/Docker.md)
> - 🔧 [反向代理設定](部署指南/反向代理.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,9 +4,9 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - ⚙️ [環境變數設定](../configuration/environment-variables.md)
> - 🔒 [安全性設定](../configuration/security.md)
> - 🧹 [清理與限制](../configuration/limits-and-cleanup.md)
> - ⚙️ [環境變數設定](../配置設定/環境變數.md)
> - 🔒 [安全性設定](../配置設定/安全性.md)
> - 🧹 [清理與限制](../配置設定/清理與限制.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,7 +4,7 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🔒 [安全性設定](../configuration/security.md)
> - 🔒 [安全性設定](../配置設定/安全性.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,9 +4,9 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🔌 [轉換器完整說明](features/converters.md)
> - 📊 [OCR 功能](features/ocr.md)
> - 🌐 [翻譯功能](features/translation.md)
> - 🔌 [轉換器完整說明](功能說明/轉換器.md)
> - 📊 [OCR 功能](功能說明/OCR.md)
> - 🌐 [翻譯功能](功能說明/翻譯.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,9 +4,9 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🐳 [Docker 部署](deployment/docker.md)
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
> - 🔒 [安全性設定](configuration/security.md)
> - 🐳 [Docker 部署](部署指南/Docker.md)
> - 🔧 [反向代理設定](部署指南/反向代理.md)
> - 🔒 [安全性設定](配置設定/安全性.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,8 +4,8 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 📦 [Docker 部署指南](deployment/docker.md)
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
> - 📦 [Docker 部署指南](部署指南/Docker.md)
> - 🔧 [反向代理設定](部署指南/反向代理.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,7 +4,7 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - ❓ [常見問題 FAQ](getting-started/faq.md)
> - ❓ [常見問題 FAQ](快速入門/常見問題.md)
>
> 此文件將在未來版本中移除。

View file

@ -1,118 +0,0 @@
# 翻譯功能
ConvertX-CN 內建 PDFMathTranslate 引擎,可翻譯 PDF 同時保留數學公式與排版。
---
## 功能特色
- 📊 **保留數學公式**LaTeX 公式完整保留
- 📈 **保留圖表**:圖片、表格位置不變
- 📑 **保留目錄**:連結與結構完整
- 🌐 **多語言支援**15+ 種目標語言
---
## 支援的目標語言
| 格式代碼 | 目標語言 |
| ----------- | -------- |
| `pdf-en` | 英文 |
| `pdf-zh` | 簡體中文 |
| `pdf-zh-TW` | 繁體中文 |
| `pdf-ja` | 日文 |
| `pdf-ko` | 韓文 |
| `pdf-de` | 德文 |
| `pdf-fr` | 法文 |
| `pdf-es` | 西班牙文 |
| `pdf-it` | 義大利文 |
| `pdf-pt` | 葡萄牙文 |
| `pdf-ru` | 俄文 |
| `pdf-ar` | 阿拉伯文 |
| `pdf-hi` | 印地文 |
| `pdf-vi` | 越南文 |
| `pdf-th` | 泰文 |
---
## 使用方式
1. 上傳 PDF 檔案
2. 在目標格式選擇 `pdf-zh-TW`(或其他語言)
3. 點擊轉換
4. 下載翻譯後的 PDF
---
## 輸出格式
所有輸出一律打包為 `.tar` 檔案,包含:
```
output.tar
├── original.pdf # 原始 PDF
└── translated-*.pdf # 翻譯後的 PDF
```
---
## 環境變數設定
### PDFMATHTRANSLATE_SERVICE
選擇翻譯服務提供商。
| 值 | 說明 |
| -------- | ------------------- |
| `google` | Google 翻譯(預設) |
| `deepl` | DeepL 翻譯 |
| `openai` | OpenAI API |
| `azure` | Azure Translator |
```yaml
environment:
- PDFMATHTRANSLATE_SERVICE=google
```
### 使用付費服務
如需使用 DeepL 或 OpenAI 等付費服務,需設定 API Key
```yaml
environment:
- PDFMATHTRANSLATE_SERVICE=openai
- OPENAI_API_KEY=sk-xxxxx
```
---
## 適用場景
### ✅ 適合
- 學術論文翻譯
- 數學/物理教科書
- 技術文件翻譯
- AI/ML 論文
### ⚠️ 限制
- 掃描版 PDF需先 OCR
- 複雜排版的雜誌
- 手寫文件
---
## 注意事項
1. **模型已預載**:所需模型已在 Docker build 階段下載
2. **不會隱式下載**Runtime 不會下載額外模型
3. **預設免費服務**:使用 Google 翻譯(免費)
---
## 相關文件
- [支援的轉換器](converters.md)
- [OCR 功能](ocr.md)
- [環境變數設定](../configuration/environment-variables.md)

View file

@ -4,9 +4,9 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 📖 [概覽](getting-started/overview.md)
> - 🚀 [快速開始](getting-started/quick-start.md)
> - ❓ [常見問題](getting-started/faq.md)
> - 📖 [概覽](快速入門/概覽.md)
> - 🚀 [快速開始](快速入門/快速開始.md)
> - ❓ [常見問題](快速入門/常見問題.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,7 +4,7 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🌐 [多語言介面支援](features/i18n.md)
> - 🌐 [多語言介面支援](功能說明/多語言.md)
>
> 此文件將在未來版本中移除。

View file

@ -4,8 +4,8 @@
>
> 本文件內容已整合至新的文件結構,請參閱:
>
> - 🛠️ [專案結構](development/project-structure.md)
> - 🐳 [Docker 部署](deployment/docker.md)
> - 🛠️ [專案結構](開發指南/專案結構.md)
> - 🐳 [Docker 部署](部署指南/Docker.md)
>
> 此文件將在未來版本中移除。

View file

@ -1,6 +1,10 @@
# OCR 功能
ConvertX-CN 內建 Tesseract OCR可將圖片中的文字轉換為可編輯文字。
ConvertX-CN 內建 Tesseract OCR 與 ocrmypdf提供完整的 OCR 功能:
- **圖片 OCR**:將圖片中的文字轉換為可編輯文字
- **PDF OCR**:將掃描版 PDF 轉換為可搜尋 PDF加入文字層
- **PDF 自動偵測**:翻譯引擎會自動偵測掃描版 PDF 並進行 OCR 處理
---
@ -22,24 +26,51 @@ ConvertX-CN 完整版內建以下 OCR 語言:
## 使用方式
### PDF → 可搜尋 PDFOCR
將掃描版 PDF 轉換為可搜尋 PDF
1. 上傳 PDF 檔案
2. 選擇 Converter: `OCRmyPDF`
3. 選擇目標語言:
| 格式 | 說明 |
| ---- | ---- |
| `pdf-en` | English |
| `pdf-zh-TW` | 繁體中文 |
| `pdf-zh` | 簡體中文 |
| `pdf-ja` | 日本語 |
| `pdf-ko` | 한국어 |
| `pdf-de` | Deutsch |
| `pdf-fr` | Français |
4. 進行轉換
> 💡 **功能特點**
>
> - 自動偵測頁面方向並旋轉
> - 自動校正傾斜
> - 跳過已有文字層的頁面
> - 詳細的處理進度輸出
### 圖片 → 文字
1. 上傳圖片PNG, JPG, TIFF 等)
2. 選擇目標格式 `txt``pdf`(可搜尋)
3. 進行轉換
### PDF → 可搜尋 PDF
### 掃描版 PDF 翻譯(自動處理)
1. 上傳掃描版 PDF
2. 選擇 OCR 處理
3. 獲得可搜尋的 PDF
當使用 PDFMathTranslate 或 BabelDOC 翻譯 PDF 時:
1. 系統會**自動偵測**是否為掃描版 PDF無文字層
2. 若為掃描版,系統會**自動執行 OCR** 加入文字層
3. 翻譯引擎使用 OCR 處理後的 PDF 進行翻譯
4. 使用者無需手動操作,全程自動完成
---
## 支援的輸入格式
- **點陣圖**PNG, JPG, JPEG, TIFF, BMP, GIF
- **文件**PDF掃描版
- **其他**WebP, PNM, PBM
---
@ -78,14 +109,18 @@ Tesseract 可同時辨識多種語言,但準確度可能下降。
### 方法一:自訂 Dockerfile
> 💡 在 `Dockerfile.full` 中取消註解以下內容
```dockerfile
# 在 Dockerfile.full 中取消註解
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-spa \ # 西班牙文
tesseract-ocr-ita \ # 義大利文
tesseract-ocr-spa \
tesseract-ocr-ita \
&& rm -rf /var/lib/apt/lists/*
```
- `tesseract-ocr-spa` — 西班牙文
- `tesseract-ocr-ita` — 義大利文
### 方法二:掛載語言包
```yaml
@ -112,6 +147,6 @@ volumes:
## 相關文件
- [支援的轉換器](converters.md)
- [翻譯功能](translation.md)
- [Docker 部署](../deployment/docker.md)
- [支援的轉換器](轉換器.md)
- [翻譯功能](翻譯.md)
- [Docker 部署](../部署指南/Docker.md)

View file

@ -128,5 +128,5 @@ http://localhost:3000/?lang=ja
## 相關文件
- [貢獻指南](../development/contribution.md)
- [專案結構](../development/project-structure.md)
- [貢獻指南](../開發指南/貢獻指南.md)
- [專案結構](../開發指南/專案結構.md)

187
docs/功能說明/翻譯.md Normal file
View file

@ -0,0 +1,187 @@
# 翻譯功能
ConvertX-CN 內建兩個 PDF 翻譯引擎:
| 引擎 | 命令 | 輸出格式 | 特色 |
| -------------------- | ---------- | --------------------- | -------------------- |
| **PDFMathTranslate** | `pdf2zh` | PDF | 保留數學公式與排版 |
| **BabelDOC** | `babeldoc` | PDF / Markdown / HTML | 多格式輸出、快速翻譯 |
---
## PDFMathTranslate
適合學術論文、數學/物理教科書等需要保留公式的文件。
### 功能特色
- 📊 **保留數學公式**LaTeX 公式完整保留
- 📈 **保留圖表**:圖片、表格位置不變
- 📑 **保留目錄**:連結與結構完整
- 🌐 **多語言支援**15+ 種目標語言
### 使用方式
1. 上傳 PDF 檔案
2. 在目標格式選擇 `pdf-zh-TW`(或其他語言代碼)
3. 點擊轉換
4. 下載翻譯後的 PDF.tar 檔案)
---
## BabelDOC
適合一般文件翻譯,支援多種輸出格式。
### 功能特色
- 📄 **多格式輸出**PDF、Markdown、HTML
- ⚡ **快速翻譯**:針對一般文件優化
- 🔄 **格式轉換**:可同時翻譯並轉換格式
### 使用方式
1. 上傳 PDF 檔案
2. 在目標格式選擇:
- `pdf-zh-TW`:翻譯後輸出 PDF
- `md-zh-TW`:翻譯後輸出 Markdown
- `html-zh-TW`:翻譯後輸出 HTML
3. 點擊轉換
4. 下載結果(.tar 檔案)
---
## 支援的目標語言
兩個引擎都支援以下語言:
| 格式代碼 | 目標語言 |
| ----------- | -------- |
| `pdf-en` | 英文 |
| `pdf-zh` | 簡體中文 |
| `pdf-zh-TW` | 繁體中文 |
| `pdf-ja` | 日文 |
| `pdf-ko` | 韓文 |
| `pdf-de` | 德文 |
| `pdf-fr` | 法文 |
| `pdf-es` | 西班牙文 |
| `pdf-it` | 義大利文 |
| `pdf-pt` | 葡萄牙文 |
| `pdf-ru` | 俄文 |
| `pdf-ar` | 阿拉伯文 |
| `pdf-hi` | 印地文 |
| `pdf-vi` | 越南文 |
| `pdf-th` | 泰文 |
> 💡 BabelDOC 額外支援 `md-<lang>``html-<lang>` 格式
---
## 輸出格式
所有輸出一律打包為 `.tar` 檔案:
### PDFMathTranslate 輸出
```
output.tar
├── original.pdf # 原始 PDF
└── translated-*.pdf # 翻譯後的 PDF
```
### BabelDOC 輸出
```
output.tar
├── translated-*.pdf # 翻譯後的 PDF如果選擇 pdf-*
├── translated-*.md # 翻譯後的 Markdown如果選擇 md-*
└── translated-*.html # 翻譯後的 HTML如果選擇 html-*
```
---
## 環境變數設定
### 翻譯服務PDFMathTranslate
> 💡 `PDFMATHTRANSLATE_SERVICE` 可選值:`google`(預設)、`bing``deepl``openai``azure`
```yaml
environment:
- PDFMATHTRANSLATE_SERVICE=google
```
| 服務 | 說明 | 需要 API Key |
| -------- | ---------------- | ------------ |
| `google` | Google 翻譯 | ❌ 免費 |
| `bing` | Bing 翻譯 | ❌ 免費 |
| `deepl` | DeepL 翻譯 | ✅ |
| `openai` | OpenAI API | ✅ |
| `azure` | Azure Translator | ✅ |
### 使用付費服務
如需使用付費服務,需設定對應的 API Key
```yaml
environment:
- PDFMATHTRANSLATE_SERVICE=openai
- OPENAI_API_KEY=sk-xxxxx
# 或
- PDFMATHTRANSLATE_SERVICE=deepl
- DEEPL_API_KEY=xxxxx
```
---
## 適用場景
### PDFMathTranslate 適合
- ✅ 學術論文翻譯
- ✅ 數學/物理教科書
- ✅ 包含 LaTeX 公式的文件
- ✅ AI/ML 論文
### BabelDOC 適合
- ✅ 一般文件翻譯
- ✅ 需要 Markdown 輸出(方便後續編輯)
- ✅ 需要 HTML 輸出(網頁展示)
- ✅ 快速翻譯大量文件
### ⚠️ 共同限制
- 掃描版 PDF系統會自動偵測並使用 OCR 處理)
- 複雜排版的雜誌
- 手寫文件
---
## 如何選擇引擎?
| 需求 | 推薦引擎 |
| ------------------ | ---------------- |
| 有數學公式 | PDFMathTranslate |
| 需要 Markdown 輸出 | BabelDOC |
| 需要 HTML 輸出 | BabelDOC |
| 一般商業文件 | 兩者皆可 |
| 追求翻譯品質 | PDFMathTranslate |
| 追求翻譯速度 | BabelDOC |
---
## 注意事項
1. **模型已預載**:所需模型已在 Docker build 階段下載
2. **不會隱式下載**Runtime 不會下載額外模型
3. **預設免費服務**:使用 Google/Bing 翻譯(免費)
4. **離線模式**:可設定 `HF_HUB_OFFLINE=1` 確保完全離線
---
## 相關文件
- [支援的轉換器](轉換器.md)
- [OCR 功能](OCR.md)
- [環境變數設定](../配置設定/環境變數.md)

View file

@ -130,7 +130,7 @@ docker compose up -d
- 繁體中文、簡體中文
- 日文、韓文
- 英文、德文、法文
- 更多詳見 [多語言支援](../features/i18n.md)
- 更多詳見 [多語言支援](../功能說明/多語言.md)
---
@ -138,7 +138,7 @@ docker compose up -d
### Q: 支援哪些格式?
1000+ 種格式,詳見 [支援的轉換器](../features/converters.md)
1000+ 種格式,詳見 [支援的轉換器](../功能說明/轉換器.md)
### Q: 轉換失敗怎麼辦?
@ -169,11 +169,11 @@ client_max_body_size 500M;
### Q: 如何使用反向代理?
詳見 [反向代理設定](../deployment/reverse-proxy.md)
詳見 [反向代理設定](../部署指南/反向代理.md)
### Q: 如何啟用硬體加速?
詳見 [進階配置 - 硬體加速](../deployment/docker.md#硬體加速)
詳見 [進階配置 - 硬體加速](../部署指南/Docker.md#硬體加速)
### Q: 如何啟用 API Server
@ -181,7 +181,7 @@ client_max_body_size 500M;
docker compose --profile api up -d
```
詳見 [API 文件](../api/overview.md)
詳見 [API 文件](../API/總覽.md)
---

View file

@ -138,7 +138,7 @@ environment:
## 下一步
- 📖 [Docker 詳細配置](../deployment/docker.md)
- ⚙️ [環境變數設定](../configuration/environment-variables.md)
- 🔒 [安全性設定](../configuration/security.md)
- 🔧 [反向代理設定](../deployment/reverse-proxy.md)
- 📖 [Docker 詳細配置](../部署指南/Docker.md)
- ⚙️ [環境變數設定](../配置設定/環境變數.md)
- 🔒 [安全性設定](../配置設定/安全性.md)
- 🔧 [反向代理設定](../部署指南/反向代理.md)

View file

@ -78,6 +78,6 @@ ConvertX-CN 是 fork 自 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
## 下一步
- 🚀 [快速開始](quick-start.md) — 5 分鐘內完成部署
- ❓ [常見問題](faq.md) — 解決常見問題
- 📦 [Docker 部署](../deployment/docker.md) — 詳細部署指南
- 🚀 [快速開始](快速開始.md) — 5 分鐘內完成部署
- ❓ [常見問題](常見問題.md) — 解決常見問題
- 📦 [Docker 部署](../部署指南/Docker.md) — 詳細部署指南

View file

@ -143,6 +143,6 @@ docker build -t convertx-cn-test .
## 相關文件
- [測試策略](test-strategy.md)
- [E2E 測試](e2e-tests.md)
- [貢獻指南](../development/contribution.md)
- [測試策略](測試策略.md)
- [E2E 測試](E2E測試.md)
- [貢獻指南](../開發指南/貢獻指南.md)

View file

@ -176,6 +176,6 @@ npx playwright codegen http://localhost:3000
## 相關文件
- [測試策略](test-strategy.md)
- [CI/CD](ci-cd.md)
- [本地開發](../development/local-development.md)
- [測試策略](測試策略.md)
- [CI/CD](CI-CD.md)
- [本地開發](../開發指南/本地開發.md)

View file

@ -49,6 +49,6 @@ docker compose up -d
## 相關文件
- [Docker 部署](../deployment/docker.md)
- [環境變數](../configuration/environment-variables.md)
- [反向代理](../deployment/reverse-proxy.md)
- [Docker 部署](../部署指南/Docker.md)
- [環境變數](../配置設定/環境變數.md)
- [反向代理](../部署指南/反向代理.md)

View file

@ -97,21 +97,29 @@ docker run -d \
**重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。
```bash
# Linux / macOS
mkdir -p ~/convertx-cn/data
**Linux / macOS**
# Windows PowerShell
```bash
mkdir -p ~/convertx-cn/data
```
**Windows PowerShell**
```powershell
mkdir C:\convertx-cn\data
```
### 備份與還原
```bash
# 備份
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
**備份:**
# 還原
```bash
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
```
**還原:**
```bash
tar -xzvf convertx-backup-20260120.tar.gz
```
@ -199,18 +207,23 @@ services:
## 版本更新
```bash
# 拉取最新版本
docker pull convertx/convertx-cn:latest
**1. 拉取最新版本:**
# 停止並移除舊容器
```bash
docker pull convertx/convertx-cn:latest
```
**2. 停止並移除舊容器:**
```bash
docker stop convertx-cn
docker rm convertx-cn
```
# 重新啟動(使用相同的參數)
docker run -d \
--name convertx-cn \
# ... 其他參數
**3. 重新啟動(使用相同的參數):**
```bash
docker run -d --name convertx-cn ...
```
或使用 Docker Compose
@ -228,7 +241,12 @@ docker compose up -d
```bash
docker logs convertx-cn
docker logs -f convertx-cn # 持續追蹤
```
持續追蹤日誌:
```bash
docker logs -f convertx-cn
```
### 進入容器
@ -251,5 +269,5 @@ docker exec -it convertx-cn /bin/bash
## 相關文件
- [Docker Compose 詳解](docker-compose.md)
- [反向代理設定](reverse-proxy.md)
- [環境變數設定](../configuration/environment-variables.md)
- [反向代理設定](反向代理.md)
- [環境變數設定](../配置設定/環境變數.md)

View file

@ -111,7 +111,7 @@ environment:
- AUTO_DELETE_EVERY_N_HOURS=24
```
完整環境變數說明請參考 [環境變數文件](../config/environment.md)。
完整環境變數說明請參考 [環境變數文件](../配置設定/環境變數.md)。
---

View file

@ -59,6 +59,8 @@ cd C:\convertx-cn
在專案資料夾建立 `docker-compose.yml`
> 💡 `JWT_SECRET` 請改成你自己的隨機字串(至少 32 字元)
```yaml
services:
convertx:
@ -71,7 +73,7 @@ services:
- ./data:/app/data
environment:
- TZ=Asia/Taipei
- JWT_SECRET=請改成你自己的隨機字串至少32字元
- JWT_SECRET=your-random-secret-at-least-32-chars
```
### 必要參數說明
@ -119,12 +121,12 @@ docker compose logs -f # Ctrl+C 退出
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
| 3000 埠被佔用 | 改用 `"8080:3000"` |
更多問題請參考 [FAQ](../faq.md)。
更多問題請參考 [FAQ](../快速入門/常見問題.md)。
---
## 下一步
- [環境變數完整說明](../config/environment.md)
- [反向代理與 HTTPS](../deployment.md)
- [環境變數完整說明](../配置設定/環境變數.md)
- [反向代理與 HTTPS](反向代理.md)
- [版本更新方法](update.md)

View file

@ -24,12 +24,12 @@ docker compose up -d
### 更新到指定版本
修改 `docker-compose.yml`
修改 `docker-compose.yml`(指定要更新的版本號)
```yaml
services:
convertx:
image: convertx/convertx-cn:v0.1.9 # 指定版本
image: convertx/convertx-cn:v0.1.9
```
然後執行:
@ -78,12 +78,12 @@ Copy-Item -Recurse .\data .\data.backup.$(Get-Date -Format "yyyyMMdd")
## 回滾版本
如果新版本有問題,可以回滾到舊版本:
如果新版本有問題,可以回滾到舊版本(將版本號改為舊版本)
```yaml
services:
convertx:
image: convertx/convertx-cn:v0.1.8 # 舊版本
image: convertx/convertx-cn:v0.1.8
```
```bash
@ -120,6 +120,8 @@ docker rmi convertx/convertx-cn:v0.1.7
可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器:
> 💡 `--interval 86400` — 每 24 小時檢查一次更新
```yaml
services:
convertx:
@ -130,7 +132,7 @@ services:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 86400 # 每 24 小時檢查一次
command: --interval 86400
```
> ⚠️ 自動更新適合測試環境。生產環境建議手動更新,確認新版本穩定後再升級。

View file

@ -8,10 +8,13 @@
透過反向代理存取時,請設定:
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
- `HTTP_ALLOWED=false` — Proxy 已處理 HTTPS
```yaml
environment:
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
- HTTP_ALLOWED=false # Proxy 已處理 HTTPS
- TRUST_PROXY=true
- HTTP_ALLOWED=false
```
---

View file

@ -10,9 +10,11 @@
控制是否允許非 HTTPS 連線。
- `HTTP_ALLOWED=true` — 允許 HTTP不安全僅測試用
- `HTTP_ALLOWED=false` — 僅允許 HTTPS預設
```yaml
- HTTP_ALLOWED=true # 允許 HTTP不安全僅測試用
- HTTP_ALLOWED=false # 僅允許 HTTPS預設
- HTTP_ALLOWED=false
```
#### 運作原理
@ -40,9 +42,11 @@
是否信任反向代理傳來的 headers。
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
- `TRUST_PROXY=false` — 不信任(預設)
```yaml
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
- TRUST_PROXY=false # 不信任(預設)
- TRUST_PROXY=false
```
#### 運作原理
@ -69,9 +73,11 @@
### ACCOUNT_REGISTRATION
- `ACCOUNT_REGISTRATION=true` — 開放註冊
- `ACCOUNT_REGISTRATION=false` — 關閉註冊
```yaml
- ACCOUNT_REGISTRATION=true # 開放註冊
- ACCOUNT_REGISTRATION=false # 關閉註冊
- ACCOUNT_REGISTRATION=false
```
#### 建議流程
@ -95,22 +101,41 @@
#### 產生方式
```bash
# Linux / macOS
openssl rand -hex 32
**Linux / macOS**
# 輸出範例a1b2c3d4e5f6789...64 字元)
```bash
openssl rand -hex 32
```
**Windows PowerShell**
```powershell
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
```
**線上工具:**
- https://generate-secret.vercel.app/32
**Node.js**
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
> 💡 產生的密鑰應為 32-64 字元的隨機字串,例如:`a1b2c3d4e5f6789...`
---
## 公開服務安全
### ALLOW_UNAUTHENTICATED
- `ALLOW_UNAUTHENTICATED=true` — 允許未登入使用
- `ALLOW_UNAUTHENTICATED=false` — 必須登入(預設)
```yaml
- ALLOW_UNAUTHENTICATED=true # 允許未登入使用
- ALLOW_UNAUTHENTICATED=false # 必須登入(預設)
- ALLOW_UNAUTHENTICATED=false
```
#### 風險
@ -125,12 +150,16 @@ openssl rand -hex 32
若需要公開服務,建議:
- `AUTO_DELETE_EVERY_N_HOURS=1` — 頻繁清理
- `HIDE_HISTORY=true` — 隱藏歷史
- `MAX_CONVERT_PROCESS=2` — 限制同時轉換數
```yaml
environment:
- ALLOW_UNAUTHENTICATED=true
- AUTO_DELETE_EVERY_N_HOURS=1 # 頻繁清理
- HIDE_HISTORY=true # 隱藏歷史
- MAX_CONVERT_PROCESS=2 # 限制同時轉換數
- AUTO_DELETE_EVERY_N_HOURS=1
- HIDE_HISTORY=true
- MAX_CONVERT_PROCESS=2
```
---
@ -139,31 +168,36 @@ environment:
### 防火牆
只開放必要的埠:
只開放必要的埠。
**UFW (Ubuntu) 開放單一埠:**
```bash
# UFW (Ubuntu)
sudo ufw allow 3000/tcp
```
# 或只允許特定 IP
**只允許特定 IP 範圍:**
```bash
sudo ufw allow from 192.168.1.0/24 to any port 3000
```
### 只允許本機存取
> 💡 `127.0.0.1:3000:3000` 只有本機可存取
```yaml
ports:
- "127.0.0.1:3000:3000" # 只有本機可存取
- "127.0.0.1:3000:3000"
```
搭配反向代理提供對外服務。
### 限制上傳大小
在反向代理層限制:
在反向代理層限制Nginx 範例)
```nginx
# Nginx
client_max_body_size 100M;
```
@ -173,21 +207,25 @@ client_max_body_size 100M;
### 定期清理
> 💡 每 24 小時自動清理過期檔案
```yaml
- AUTO_DELETE_EVERY_N_HOURS=24 # 每 24 小時清理
- AUTO_DELETE_EVERY_N_HOURS=24
```
### 備份
使用 crontab 設定定期備份(每天凌晨 2 點):
```bash
# 定期備份資料
0 2 * * * tar -czvf /backup/convertx-$(date +\%Y\%m\%d).tar.gz /path/to/data
```
### 權限設定
限制資料夾權限,僅允許擁有者存取:
```bash
# 限制資料夾權限
chmod 700 ./data
```
@ -214,6 +252,6 @@ chmod 700 ./data
## 相關文件
- [環境變數設定](environment-variables.md)
- [反向代理設定](../deployment/reverse-proxy.md)
- [Docker 部署](../deployment/docker.md)
- [環境變數設定](環境變數.md)
- [反向代理設定](../部署指南/反向代理.md)
- [Docker 部署](../部署指南/Docker.md)

View file

@ -16,16 +16,13 @@
| 停用 | `0` |
| 單位 | 小時 |
- `AUTO_DELETE_EVERY_N_HOURS=24` — 每 24 小時清理一次
- `AUTO_DELETE_EVERY_N_HOURS=1` — 每 1 小時清理一次(公開服務建議)
- `AUTO_DELETE_EVERY_N_HOURS=0` — 停用自動清理
```yaml
environment:
# 每 24 小時清理一次
- AUTO_DELETE_EVERY_N_HOURS=24
# 每 1 小時清理一次(公開服務建議)
- AUTO_DELETE_EVERY_N_HOURS=1
# 停用自動清理
- AUTO_DELETE_EVERY_N_HOURS=0
```
### 清理範圍
@ -49,13 +46,12 @@ environment:
| ------ | ------------- |
| 預設值 | `0`(無限制) |
- `MAX_CONVERT_PROCESS=2` — 最多同時 2 個轉換任務
- `MAX_CONVERT_PROCESS=0` — 無限制
```yaml
environment:
# 最多同時 2 個轉換任務
- MAX_CONVERT_PROCESS=2
# 無限制
- MAX_CONVERT_PROCESS=0
```
#### 使用建議
@ -152,17 +148,22 @@ find ./data/output -mtime +7 -delete
### 個人使用
- `AUTO_DELETE_EVERY_N_HOURS=168` — 一週清理一次
- `MAX_CONVERT_PROCESS=0` — 無限制
```yaml
environment:
- AUTO_DELETE_EVERY_N_HOURS=168 # 一週
- MAX_CONVERT_PROCESS=0 # 無限制
- AUTO_DELETE_EVERY_N_HOURS=168
- MAX_CONVERT_PROCESS=0
```
### 小型團隊
> 💡 `AUTO_DELETE_EVERY_N_HOURS=24` — 每天清理一次
```yaml
environment:
- AUTO_DELETE_EVERY_N_HOURS=24 # 一天
- AUTO_DELETE_EVERY_N_HOURS=24
- MAX_CONVERT_PROCESS=4
deploy:
@ -173,9 +174,11 @@ deploy:
### 公開服務
> 💡 `AUTO_DELETE_EVERY_N_HOURS=1` — 每小時清理一次
```yaml
environment:
- AUTO_DELETE_EVERY_N_HOURS=1 # 一小時
- AUTO_DELETE_EVERY_N_HOURS=1
- MAX_CONVERT_PROCESS=2
- ALLOW_UNAUTHENTICATED=true
- HIDE_HISTORY=true
@ -191,6 +194,6 @@ deploy:
## 相關文件
- [環境變數設定](environment-variables.md)
- [安全性設定](security.md)
- [Docker 部署](../deployment/docker.md)
- [環境變數設定](環境變數.md)
- [安全性設定](安全性.md)
- [Docker 部署](../部署指南/Docker.md)

View file

@ -174,6 +174,6 @@ api-server/
## 相關文件
- [本地開發](local-development.md)
- [貢獻指南](contribution.md)
- [測試策略](../testing/test-strategy.md)
- [本地開發](本地開發.md)
- [貢獻指南](貢獻指南.md)
- [測試策略](../測試/測試策略.md)

View file

@ -187,6 +187,6 @@ docs: update deployment guide
## 相關文件
- [專案結構](project-structure.md)
- [本地開發](local-development.md)
- [測試策略](../testing/test-strategy.md)
- [專案結構](專案結構.md)
- [本地開發](本地開發.md)
- [測試策略](../測試/測試策略.md)

View file

@ -1,6 +1,6 @@
{
"name": "convertx-frontend",
"version": "0.1.13",
"version": "0.1.14",
"scripts": {
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
@ -60,5 +60,6 @@
"@parcel/watcher",
"@tailwindcss/oxide",
"oxc-resolver"
]
],
"license": "AGPL-3.0"
}

View file

@ -11,6 +11,7 @@ import {
import { join, basename, dirname } from "node:path";
import { ExecFileFn } from "./types";
import { getArchiveFileName } from "../transfer";
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
/**
* BabelDOC Content Engine
@ -275,7 +276,19 @@ export async function convert(
_options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
let ocrTempFile: string | undefined;
try {
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
console.log(`[BabelDOC] Checking if PDF needs OCR...`);
const ocrResult = await ensureSearchablePdf(filePath, execFile);
const inputPdf = ocrResult.path;
ocrTempFile = ocrResult.tempFile;
if (ocrResult.wasOcred) {
console.log(`[BabelDOC] ✅ Scanned PDF detected and OCR'd automatically`);
}
// 1. 檢查資源(警告但不阻止)
checkResourcesExist();
@ -300,8 +313,8 @@ export async function convert(
// 5. 設定 BabelDOC 輸出路徑(依輸出格式決定副檔名)
const translatedFilePath = join(tempDir, `${inputFileName}-translated.${outputExt}`);
// 6. 執行 babeldoc 翻譯
await runBabelDoc(filePath, translatedFilePath, targetLang, outputFormat, execFile);
// 6. 執行 babeldoc 翻譯(使用 OCR 處理後的 PDF
await runBabelDoc(inputPdf, translatedFilePath, targetLang, outputFormat, execFile);
// 7. 複製翻譯後的檔案到封裝目錄
const translatedDest = join(archiveDir, `translated-${targetLang}.${outputExt}`);
@ -345,8 +358,13 @@ export async function convert(
// 10. 清理臨時目錄
removeDir(tempDir);
// 11. 清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
return "Done";
} catch (error) {
// 確保清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
throw new Error(`BabelDOC error: ${error}`);
}
}

View file

@ -31,6 +31,7 @@ import {
properties as propertiesPDFMathTranslate,
} from "./pdfmathtranslate";
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
import { convert as convertOcrMyPdf, properties as propertiesOcrMyPdf } from "./ocrmypdf";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
@ -156,6 +157,10 @@ const properties: Record<
properties: propertiesBabelDoc,
converter: convertBabelDoc,
},
OCRmyPDF: {
properties: propertiesOcrMyPdf,
converter: convertOcrMyPdf,
},
};
function chunks<T>(arr: T[], size: number): T[][] {

271
src/converters/ocrmypdf.ts Normal file
View file

@ -0,0 +1,271 @@
import { execFile as execFileOriginal } from "node:child_process";
import { existsSync, mkdirSync, copyFileSync, statSync } from "node:fs";
import { dirname, basename } from "node:path";
import type { ExecFileFn } from "./types";
/**
* OCRmyPDF Content Engine
*
* PDF PDF
* 使 Tesseract OCR
*
* OCR Docker
* - eng: 英文
* - chi_tra: 繁體中文
* - chi_sim: 簡體中文
* - jpn: 日文
* - kor: 韓文
* - deu: 德文
* - fra: 法文
*
* UI PDFMathTranslate
* pdf-en, pdf-zh-TW, pdf-ja
*/
// 內建支援的 OCR 語言(與 PDFMathTranslate 風格一致)
const SUPPORTED_LANGUAGES = [
"en", // English
"zh-TW", // 繁體中文
"zh", // 簡體中文
"ja", // 日本語
"ko", // 한국어
"de", // Deutsch
"fr", // Français
] as const;
// Tesseract 語言代碼映射UI 代碼 → Tesseract 代碼)
const LANG_MAP: Record<string, string> = {
"en": "eng",
"zh-TW": "chi_tra",
"zh": "chi_sim",
"ja": "jpn",
"ko": "kor",
"de": "deu",
"fr": "fra",
};
export const properties = {
from: {
document: ["pdf"],
},
to: {
document: SUPPORTED_LANGUAGES.map((lang) => `pdf-${lang}`),
},
};
/**
* convertTo OCR
* @param convertTo "pdf-en" "pdf-zh-TW"
* @returns Tesseract OCR
*/
function extractOcrLanguage(convertTo: string): string {
// convertTo 格式: pdf-<lang>
const match = convertTo.match(/^pdf-(.+)$/);
if (!match || !match[1]) {
throw new Error(`Invalid convertTo format: ${convertTo}. Expected pdf-<lang>`);
}
const uiLang = match[1];
// 轉換為 Tesseract 語言代碼
const tessLang = LANG_MAP[uiLang] || uiLang.replace(/-/g, "_");
return tessLang;
}
/**
*
*/
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
/**
* ocrmypdf PDF OCR
*
* @param inputPath PDF
* @param outputPath PDF
* @param lang OCR
* @param execFile execFile
*/
function runOcrMyPdf(
inputPath: string,
outputPath: string,
lang: string,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
console.log(`[OCRmyPDF] ========================================`);
console.log(`[OCRmyPDF] 🔍 開始 PDF OCR 處理`);
console.log(`[OCRmyPDF] ========================================`);
// 階段 1驗證輸入檔案
console.log(`[OCRmyPDF] 📋 階段 1/5驗證輸入檔案`);
if (!existsSync(inputPath)) {
reject(new Error(`Input file not found: ${inputPath}`));
return;
}
const inputStats = statSync(inputPath);
console.log(`[OCRmyPDF] ✅ 輸入檔案: ${basename(inputPath)}`);
console.log(`[OCRmyPDF] ✅ 檔案大小: ${formatFileSize(inputStats.size)}`);
// 階段 2準備 OCR 參數
console.log(`[OCRmyPDF] 📋 階段 2/5準備 OCR 參數`);
console.log(`[OCRmyPDF] ✅ OCR 語言: ${lang}`);
const args = [
"-l", lang,
"--skip-text", // 跳過已有文字的頁面
"--optimize", "1", // 輕度優化
"--deskew", // 自動校正傾斜
"--rotate-pages", // 自動偵測頁面方向
"--jobs", "2", // 使用 2 個並行處理
inputPath,
outputPath,
];
console.log(`[OCRmyPDF] ✅ 參數: --skip-text --optimize 1 --deskew --rotate-pages`);
// 階段 3執行 OCR
console.log(`[OCRmyPDF] 📋 階段 3/5執行 Tesseract OCR...`);
console.log(`[OCRmyPDF] ⏳ 處理中(大型 PDF 可能需要數分鐘)...`);
const startTime = Date.now();
execFile("ocrmypdf", args, (error: Error | null, stdout: string, stderr: string) => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (error) {
// 階段 3 失敗處理
console.log(`[OCRmyPDF] ❌ OCR 處理失敗(耗時 ${elapsed}s`);
// 檢查特定錯誤類型
if (stderr && stderr.includes("PriorOcrFoundError")) {
console.log(`[OCRmyPDF] 📋 階段 4/5PDF 已有文字層,跳過 OCR`);
console.log(`[OCRmyPDF] ✅ 複製原始檔案...`);
try {
copyFileSync(inputPath, outputPath);
console.log(`[OCRmyPDF] 📋 階段 5/5完成`);
console.log(`[OCRmyPDF] ✅ 輸出: ${basename(outputPath)}`);
console.log(`[OCRmyPDF] ========================================`);
console.log(`[OCRmyPDF] ✅ OCR 完成PDF 已有文字層)`);
console.log(`[OCRmyPDF] ========================================`);
resolve(outputPath);
return;
} catch (copyError) {
reject(new Error(`Failed to copy already-OCRed PDF: ${copyError}`));
return;
}
}
if (stderr && stderr.includes("EncryptedPdfError")) {
console.log(`[OCRmyPDF] ❌ PDF 已加密,無法處理`);
reject(new Error("PDF is encrypted. Please decrypt it first."));
return;
}
if (stderr && stderr.includes("InputFileError")) {
console.log(`[OCRmyPDF] ❌ PDF 檔案無效或損壞`);
reject(new Error("Invalid or corrupted PDF file."));
return;
}
console.log(`[OCRmyPDF] 錯誤訊息: ${stderr || error.message}`);
reject(new Error(`ocrmypdf error: ${error}\nstderr: ${stderr}`));
return;
}
// 階段 3 成功
console.log(`[OCRmyPDF] ✅ OCR 引擎處理完成(耗時 ${elapsed}s`);
if (stdout) {
console.log(`[OCRmyPDF] stdout: ${stdout}`);
}
// 階段 4驗證輸出
console.log(`[OCRmyPDF] 📋 階段 4/5驗證輸出檔案`);
if (!existsSync(outputPath)) {
console.log(`[OCRmyPDF] ❌ 輸出檔案未生成`);
reject(new Error(`OCR output file not found: ${outputPath}`));
return;
}
const outputStats = statSync(outputPath);
console.log(`[OCRmyPDF] ✅ 輸出檔案: ${basename(outputPath)}`);
console.log(`[OCRmyPDF] ✅ 檔案大小: ${formatFileSize(outputStats.size)}`);
// 階段 5完成
console.log(`[OCRmyPDF] 📋 階段 5/5處理完成`);
console.log(`[OCRmyPDF] ========================================`);
console.log(`[OCRmyPDF] ✅ OCR 處理成功完成!`);
console.log(`[OCRmyPDF] 📥 輸入: ${formatFileSize(inputStats.size)}`);
console.log(`[OCRmyPDF] 📤 輸出: ${formatFileSize(outputStats.size)}`);
console.log(`[OCRmyPDF] ⏱️ 耗時: ${elapsed}s`);
console.log(`[OCRmyPDF] ========================================`);
resolve(outputPath);
});
});
}
/**
*
*
* @param filePath PDF
* @param fileType "pdf"
* @param convertTo "pdf-en""pdf-zh-TW"
* @param targetPath
* @param _options
* @param execFile execFile
*/
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
_options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
console.log(``);
console.log(`[OCRmyPDF] ╔════════════════════════════════════════╗`);
console.log(`[OCRmyPDF] ║ OCRmyPDF - PDF OCR 處理引擎 ║`);
console.log(`[OCRmyPDF] ╚════════════════════════════════════════╝`);
console.log(``);
try {
// 步驟 1解析參數
console.log(`[OCRmyPDF] 🔧 解析轉換參數...`);
const ocrLang = extractOcrLanguage(convertTo);
console.log(`[OCRmyPDF] 輸入格式: ${fileType}`);
console.log(`[OCRmyPDF] 目標格式: ${convertTo}`);
console.log(`[OCRmyPDF] OCR 語言: ${ocrLang}`);
// 步驟 2確保輸出目錄存在
console.log(`[OCRmyPDF] 📁 準備輸出目錄...`);
const outputDir = dirname(targetPath);
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
console.log(`[OCRmyPDF] ✅ 建立目錄: ${outputDir}`);
} else {
console.log(`[OCRmyPDF] ✅ 目錄已存在`);
}
// 步驟 3執行 OCR
console.log(`[OCRmyPDF] 🚀 開始 OCR 處理...`);
await runOcrMyPdf(filePath, targetPath, ocrLang, execFile);
console.log(``);
console.log(`[OCRmyPDF] ╔════════════════════════════════════════╗`);
console.log(`[OCRmyPDF] ║ ✅ PDF OCR 處理完成! ║`);
console.log(`[OCRmyPDF] ╚════════════════════════════════════════╝`);
console.log(``);
return "Done";
} catch (error) {
console.log(``);
console.log(`[OCRmyPDF] ╔════════════════════════════════════════╗`);
console.log(`[OCRmyPDF] ║ ❌ PDF OCR 處理失敗 ║`);
console.log(`[OCRmyPDF] ╚════════════════════════════════════════╝`);
console.log(`[OCRmyPDF] 錯誤: ${error}`);
console.log(``);
throw new Error(`OCRmyPDF error: ${error}`);
}
}

View file

@ -2,6 +2,7 @@ import { execFile as execFileOriginal } from "node:child_process";
import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, copyFileSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import { getArchiveFileName } from "../transfer";
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
import type { ExecFileFn } from "./types";
// 翻譯服務優先順序(自動 fallback
@ -339,7 +340,19 @@ export async function convert(
_options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
let ocrTempFile: string | undefined;
try {
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
console.log(`[PDFMathTranslate] Checking if PDF needs OCR...`);
const ocrResult = await ensureSearchablePdf(filePath, execFile);
const inputPdf = ocrResult.path;
ocrTempFile = ocrResult.tempFile;
if (ocrResult.wasOcred) {
console.log(`[PDFMathTranslate] ✅ Scanned PDF detected and OCR'd automatically`);
}
// 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
checkModelsExist();
@ -361,8 +374,8 @@ export async function convert(
const archiveDir = join(tempDir, "archive");
mkdirSync(archiveDir, { recursive: true });
// 5. 執行 pdf2zh 翻譯
const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile);
// 5. 執行 pdf2zh 翻譯(使用 OCR 處理後的 PDF
const { monoPath, dualPath } = await runPdf2zh(inputPdf, tempDir, targetLang, execFile);
// 6. 複製翻譯後的檔案到封裝目錄
// PDFMathTranslate 輸出:
@ -440,8 +453,13 @@ export async function convert(
// 9. 清理臨時目錄
removeDir(tempDir);
// 10. 清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
return "Done";
} catch (error) {
// 確保清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
throw new Error(`PDFMathTranslate error: ${error}`);
}
}

202
src/helpers/pdfOcr.ts Normal file
View file

@ -0,0 +1,202 @@
import { execFile as execFileOriginal } from "node:child_process";
import { existsSync, unlinkSync, readFileSync } from "node:fs";
import { join, dirname, basename } from "node:path";
import type { ExecFileFn } from "../converters/types";
/**
* PDF OCR
*
* PDF 使 ocrmypdf OCR
*
*/
// OCR 偵測閾值(文字字元數低於此值視為掃描版)
const SCANNED_PDF_TEXT_THRESHOLD = 100;
// 預設 OCR 語言(可透過環境變數覆蓋)
const DEFAULT_OCR_LANG = process.env.OCR_LANG || "eng+chi_tra+chi_sim+jpn";
/**
* 使 pdftotext PDF
*
* @param pdfPath PDF
* @param execFile execFile
* @returns
*/
function extractTextFromPdf(
pdfPath: string,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
// pdftotext -layout <input.pdf> -
// -layout: 保持排版
// -: 輸出到 stdout
execFile("pdftotext", ["-layout", pdfPath, "-"], (error, stdout, stderr) => {
if (error) {
// pdftotext 失敗可能是因為 PDF 沒有文字層
console.warn(`[PDF-OCR] pdftotext failed: ${stderr}`);
resolve(""); // 返回空字串,視為掃描版
return;
}
resolve(stdout || "");
});
});
}
/**
* PDF
*
* @param pdfPath PDF
* @param execFile execFile
* @returns PDF
*/
export async function isScannedPdf(
pdfPath: string,
execFile: ExecFileFn = execFileOriginal,
): Promise<boolean> {
try {
const text = await extractTextFromPdf(pdfPath, execFile);
// 移除空白字元後計算有效字元數
const cleanText = text.replace(/\s+/g, "");
const charCount = cleanText.length;
console.log(`[PDF-OCR] Extracted ${charCount} characters from PDF`);
// 如果字元數低於閾值,視為掃描版
const isScanned = charCount < SCANNED_PDF_TEXT_THRESHOLD;
if (isScanned) {
console.log(`[PDF-OCR] ⚠️ Detected scanned PDF (chars: ${charCount} < ${SCANNED_PDF_TEXT_THRESHOLD})`);
} else {
console.log(`[PDF-OCR] ✅ PDF has text layer (chars: ${charCount})`);
}
return isScanned;
} catch (error) {
console.warn(`[PDF-OCR] Detection failed, assuming scanned: ${error}`);
return true; // 偵測失敗時,保守起見視為掃描版
}
}
/**
* 使 ocrmypdf PDF OCR
*
* @param inputPath PDF
* @param outputPath PDF
* @param lang OCR eng+chi_tra+chi_sim+jpn
* @param execFile execFile
* @returns PDF
*/
export function runOcrMyPdf(
inputPath: string,
outputPath: string,
lang: string = DEFAULT_OCR_LANG,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
// ocrmypdf 參數:
// -l <lang>: OCR 語言(可用 + 連接多語言)
// --skip-text: 跳過已有文字的頁面(避免重複 OCR
// --optimize 1: 輕度優化,平衡速度和品質
// --deskew: 自動校正傾斜
// --clean: 清理背景雜訊
// --force-ocr: 強制對所有頁面進行 OCR即使有文字層
const args = [
"-l", lang,
"--skip-text",
"--optimize", "1",
"--deskew",
inputPath,
outputPath,
];
console.log(`[PDF-OCR] Running: ocrmypdf ${args.join(" ")}`);
execFile("ocrmypdf", args, (error: Error | null, stdout: string, stderr: string) => {
if (error) {
// 檢查是否是「PDF 已經有文字」的警告
if (stderr && stderr.includes("page already has text")) {
console.log(`[PDF-OCR] PDF already has text layer, using original`);
resolve(inputPath);
return;
}
reject(new Error(`ocrmypdf error: ${error}\nstderr: ${stderr}`));
return;
}
if (stdout) {
console.log(`[PDF-OCR] stdout: ${stdout}`);
}
if (stderr) {
// ocrmypdf 的進度資訊會輸出到 stderr
console.log(`[PDF-OCR] stderr: ${stderr}`);
}
if (!existsSync(outputPath)) {
reject(new Error(`OCR output file not found: ${outputPath}`));
return;
}
console.log(`[PDF-OCR] ✅ OCR completed: ${outputPath}`);
resolve(outputPath);
});
});
}
/**
* PDF
*
* PDF OCR
* PDF
*
* @param inputPath PDF
* @param execFile execFile
* @returns PDF OCR
*/
export async function ensureSearchablePdf(
inputPath: string,
execFile: ExecFileFn = execFileOriginal,
): Promise<{ path: string; wasOcred: boolean; tempFile?: string }> {
// 1. 偵測是否為掃描版
const scanned = await isScannedPdf(inputPath, execFile);
if (!scanned) {
// PDF 已有文字層,直接使用
return { path: inputPath, wasOcred: false };
}
// 2. 建立 OCR 輸出路徑
const dir = dirname(inputPath);
const name = basename(inputPath, ".pdf");
const ocrOutputPath = join(dir, `${name}_ocr_${Date.now()}.pdf`);
// 3. 執行 OCR
console.log(`[PDF-OCR] Starting OCR for scanned PDF...`);
await runOcrMyPdf(inputPath, ocrOutputPath, DEFAULT_OCR_LANG, execFile);
return {
path: ocrOutputPath,
wasOcred: true,
tempFile: ocrOutputPath, // 呼叫者需要在完成後清理此暫存檔
};
}
/**
* OCR
*
* @param tempFile
*/
export function cleanupOcrTempFile(tempFile: string | undefined): void {
if (tempFile && existsSync(tempFile)) {
try {
unlinkSync(tempFile);
console.log(`[PDF-OCR] Cleaned up temp file: ${tempFile}`);
} catch (error) {
console.warn(`[PDF-OCR] Failed to cleanup temp file: ${error}`);
}
}
}

View file

@ -67,7 +67,10 @@ describe("BabelDOC converter - Chinese translation", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
// 返回超過 100 個字元,表示 PDF 不需要 OCR
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
babeldocCalled = true;
babeldocArgs = args;
@ -131,7 +134,9 @@ describe("BabelDOC converter - English translation", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
babeldocArgs = args;
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -181,7 +186,9 @@ describe("BabelDOC converter - Traditional Chinese", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
babeldocArgs = args;
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -231,7 +238,9 @@ describe("BabelDOC converter - Output structure", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {
const outputPath = args[outputIndex + 1];
@ -267,7 +276,9 @@ describe("BabelDOC converter - Output structure", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {
const outputPath = args[outputIndex + 1];
@ -317,7 +328,9 @@ describe("BabelDOC converter - Error handling", () => {
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
const error = new Error("Translation failed") as ExecFileException;
error.code = 1;
callback(error, "", "babeldoc: Translation error");
@ -337,7 +350,9 @@ describe("BabelDOC converter - Error handling", () => {
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
// Don't create output file, simulating failed translation
callback(null, "Complete", "");
}
@ -376,7 +391,9 @@ describe("BabelDOC converter - Markdown output format", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
babeldocArgs = args;
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -428,7 +445,9 @@ describe("BabelDOC converter - HTML output format", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "babeldoc") {
if (cmd === "pdftotext") {
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
} else if (cmd === "babeldoc") {
babeldocArgs = args;
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) {

View file

@ -55,7 +55,7 @@ describe("PDFMathTranslate converter - Chinese translation", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
pdf2zhCalled = true;
pdf2zhArgs = args;
@ -116,7 +116,7 @@ describe("PDFMathTranslate converter - English translation", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -165,7 +165,7 @@ describe("PDFMathTranslate converter - Japanese translation", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -214,7 +214,7 @@ describe("PDFMathTranslate converter - Traditional Chinese", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -264,7 +264,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
const outputDir = args[outputDirIndex + 1];
@ -308,7 +308,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
const outputDir = args[outputDirIndex + 1];
@ -362,7 +362,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
const error = new Error("Translation failed") as ExecFileException;
callback(error, "", "Error: Translation service unavailable");
}
@ -381,7 +381,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdf2zh") {
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
// Don't create any output files
callback(null, "Complete", "");
} else if (cmd === "tar") {