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

328
LICENSE
View file

@ -1,35 +1,35 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
Preamble Preamble
The GNU Affero General Public License is a free, copyleft license for The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software. cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, 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 share and change all versions of a program--to make sure it remains free
software for all its users. software for all its users.
When we speak of free software, we are referring to freedom, not When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you 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 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. free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights Developers that use the GNU GPL protect your rights with two steps:
with two steps: (1) assert copyright on the software, and (2) offer (1) assert copyright on the software, and (2) offer you this License
you this License which gives you legal permission to copy, distribute which gives you legal permission to copy, distribute and/or modify the
and/or modify the software. software.
A secondary benefit of defending all users' freedom is that A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and incorporate. Many developers of free software are heartened and
@ -39,7 +39,7 @@ The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its letting the public access it on a server without ever releasing its
source code to the public. source code to the public.
The GNU Affero General Public License is designed specifically to The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the provide the source code of the modified version running there to the
@ -47,48 +47,49 @@ users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source a publicly accessible server, gives the public access to the source
code of the modified version. code of the modified version.
An older license, called the Affero General Public License and An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is published by Affero, was designed to accomplish similar goals. This
a different license, not a version of the Affero GPL, but Affero has is a different license, not a version of the Affero GPL, but Affero
released a new version of the Affero GPL which permits relicensing under has released a new version of the Affero GPL which permits relicensing
this license. under this license.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow.
TERMS AND CONDITIONS TERMS AND CONDITIONS
0. Definitions. 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 "Copyright" also means copyright-like laws that apply to other kinds
works, such as semiconductor masks. of works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this "The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations. "recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work To "modify" a work means to copy from or adapt all or part of the
in a fashion requiring copyright permission, other than the making of an work in a fashion requiring copyright permission, other than the making
exact copy. The resulting work is called a "modified version" of the of an exact copy. The resulting work is called a "modified version"
earlier work or a work "based on" the earlier work. of the earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based A "covered work" means either the unmodified Program or a work based
on the Program. on the Program.
To "propagate" a work means to do anything with it that, without To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying, computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the distribution (with or without modification), making available to the
public, and in some countries other activities as well. public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying. a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2) feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the tells the user that there is no warranty for the work (except to the
@ -97,18 +98,18 @@ work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion. menu, a prominent item in the list meets this criterion.
1. Source Code. 1. Source Code.
The "source code" for a work means the preferred form of the work The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source for making modifications to it. "Object code" means any non-source
form of a work. form of a work.
A "Standard Interface" means an interface that either is an official A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that interfaces specified for a particular programming language, one that
is widely used among developers working in that language. is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that Component, and (b) serves only to enable use of the work with that
@ -119,7 +120,7 @@ implementation is available to the public in source code form. A
(if any) on which the executable work runs, or a compiler used to (if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it. produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's control those activities. However, it does not include the work's
@ -132,16 +133,16 @@ linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those such as by intimate data communication or control flow between those
subprograms and other parts of the work. subprograms and other parts of the work.
The Corresponding Source need not include anything that users The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding can regenerate automatically from other parts of the Corresponding
Source. Source.
The Corresponding Source for a work in source code form is that The Corresponding Source for a work in source code form is that
same work. same work.
2. Basic Permissions. 2. Basic Permissions.
All rights granted under this License are granted for the term of All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a permission to run the unmodified Program. The output from running a
@ -149,7 +150,7 @@ covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law. rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you of having them make modifications exclusively for you, or provide you
@ -160,19 +161,19 @@ for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you. your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10 the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary. makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law. 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or 11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such similar laws prohibiting or restricting circumvention of such
measures. measures.
When you convey a covered work, you waive any legal power to forbid When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or the covered work, and you disclaim any intention to limit operation or
@ -180,9 +181,9 @@ modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of users, your or third parties' legal rights to forbid circumvention of
technological measures. technological measures.
4. Conveying Verbatim Copies. 4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice; appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any keep intact all notices stating that this License and any
@ -190,12 +191,12 @@ non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program. recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee. and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions. 5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions: terms of section 4, provided that you also meet all of these conditions:
@ -220,7 +221,7 @@ terms of section 4, provided that you also meet all of these conditions:
interfaces that do not display Appropriate Legal Notices, your interfaces that do not display Appropriate Legal Notices, your
work need not make them do so. work need not make them do so.
A compilation of a covered work with other separate and independent A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work, works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program, and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an in or on a volume of a storage or distribution medium, is called an
@ -230,9 +231,9 @@ beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other in an aggregate does not cause this License to apply to the other
parts of the aggregate. parts of the aggregate.
6. Conveying Non-Source Forms. 6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License, machine-readable Corresponding Source under the terms of this License,
in one of these ways: in one of these ways:
@ -278,11 +279,11 @@ in one of these ways:
Source of the work are being offered to the general public at no Source of the work are being offered to the general public at no
charge under subsection 6d. charge under subsection 6d.
A separable portion of the object code, whose source code is excluded A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be from the Corresponding Source as a System Library, need not be
included in conveying the object code work. included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family, tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product, into a dwelling. In determining whether a product is a consumer product,
@ -295,7 +296,7 @@ is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product. the only significant mode of use of the product.
"Installation Information" for a User Product means any methods, "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must a modified version of its Corresponding Source. The information must
@ -303,7 +304,7 @@ suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because code is in no case prevented or interfered with solely because
modification has been made. modification has been made.
If you convey an object code work under this section in, or with, or If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a User Product is transferred to the recipient in perpetuity or for a
@ -314,23 +315,23 @@ if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has modified object code on the User Product (for example, the work has
been installed in ROM). been installed in ROM).
The requirement to provide Installation Information does not include a The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for 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 the User Product in which it has been modified or installed. Access to
network may be denied when the modification itself materially and a network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and adversely affects the operation of the network or violates the rules and
protocols for communication across the network. protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in documented (and with an implementation available to the public in
source code form), and must require no special password or key for source code form), and must require no special password or key for
unpacking, reading or copying. unpacking, reading or copying.
7. Additional Terms. 7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions. License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent be treated as though they were included in this License, to the extent
@ -339,16 +340,16 @@ apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions. this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work, additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission. for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of add to a covered work, you may (if authorized by the copyright holders
that material) supplement the terms of this License with terms: of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or terms of sections 15 and 16 of this License; or
@ -373,7 +374,7 @@ that material) supplement the terms of this License with terms:
any liability that these contractual assumptions directly impose on any liability that these contractual assumptions directly impose on
those licensors and authors. those licensors and authors.
All other non-permissive additional terms are considered "further All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further governed by this License along with a term that is a further
@ -383,46 +384,46 @@ License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does of that license document, provided that the further restriction does
not survive such relicensing or conveying. not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating additional terms that apply to those files, or a notice indicating
where to find the applicable terms. where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions; form of a separately written license, or stated as exceptions;
the above requirements apply either way. the above requirements apply either way.
8. Termination. 8. Termination.
You may not propagate or modify a covered work except as expressly You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third this License (including any patent licenses granted under the third
paragraph of section 11). paragraph of section 11).
However, if you cease all violation of this License, then your However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a) license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation. prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice. your receipt of the notice.
Termination of your rights under this section does not terminate the Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same reinstated, you do not qualify to receive new licenses for the same
material under section 10. material under section 10.
9. Acceptance Not Required for Having Copies. 9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However, to receive a copy likewise does not require acceptance. However,
@ -431,14 +432,14 @@ modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so. covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients. 10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License. for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that work results from an entity transaction, each party to that
@ -448,7 +449,7 @@ give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts. the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation rights granted under this License, and you may not initiate litigation
@ -456,13 +457,13 @@ rights granted under this License, and you may not initiate litigation
any patent claim is infringed by making, using, selling, offering for any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it. sale, or importing the Program or any portion of it.
11. Patents. 11. Patents.
A "contributor" is a copyright holder who authorizes use under this A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version". work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version, by this License, of making, using, or selling its contributor version,
@ -472,19 +473,19 @@ purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of patent sublicenses in a manner consistent with the requirements of
this License. this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version. propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to (such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a party means to make such an agreement or commitment not to enforce a
patent against the party. patent against the party.
If you convey a covered work, knowingly relying on a patent license, If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means, publicly available network server or other readily accessible means,
@ -498,7 +499,7 @@ covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid. country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify receiving the covered work authorizing them to use, propagate, modify
@ -506,7 +507,7 @@ or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered you grant is automatically extended to all recipients of the covered
work and works based on it. work and works based on it.
A patent license is "discriminatory" if it does not include within A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered specifically granted under this License. You may not convey a covered
@ -521,95 +522,86 @@ for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement, contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007. or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law. otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom. 12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may License and any other pertinent obligations, then as a consequence you
not convey it at all. For example, if you agree to terms that obligate you may not convey it at all. For example, if you agree to terms that
to collect a royalty for further conveying from those to whom you convey obligate you to collect a royalty for further conveying from those to
the Program, the only way you could satisfy both those terms and this whom you convey the Program, the only way you could satisfy both those
License would be to refrain entirely from conveying the Program. terms and this License would be to refrain entirely from conveying the
Program.
13. Remote Network Interaction; Use with the GNU General Public License. 13. Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the Notwithstanding any other provision of this License, you have
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.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this 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, 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 but the special requirements of the GNU Affero General Public License,
3 of the GNU General Public License. section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of The Free Software Foundation may publish revised and/or new versions
the GNU Affero General Public License from time to time. Such new versions of the GNU Affero General Public License from time to time. Such new
will be similar in spirit to the present version, but may differ in detail to versions will be similar in spirit to the present version, but may
address new problems or concerns. differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General Program specifies that a certain numbered version of the GNU Affero
Public License "or any later version" applies to it, you have the General Public License "or any later version" applies to it, you have
option of following the terms and conditions either of that numbered the option of following the terms and conditions either of that
version or of any later version published by the Free Software numbered version or of any later version published by the Free
Foundation. If the Program does not specify a version number of the Software Foundation. If the Program does not specify a version number
GNU Affero General Public License, you may choose any version ever published of the GNU Affero General Public License, you may choose any version
by the Free Software Foundation. ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future 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 versions of the GNU Affero General Public License can be used, that
public statement of acceptance of a version permanently authorizes you proxy's public statement of acceptance of a version permanently
to choose that version for the Program. authorizes you to choose that version for the Program.
Later license versions may give you additional or different Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a author or copyright holder as a result of your choosing to follow a
later version. later version.
15. Disclaimer of Warranty. 15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability. 16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 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 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 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
SUCH DAMAGES. THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16. 17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms, above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the an absolute waiver of all civil liability in connection with the
@ -617,45 +609,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee. copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS 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) [![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) [![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 ## 為什麼選擇 ConvertX-CN
| 特色 | 說明 | | 特色 | 說明 |
| ----------------- | ------------------------------------ | | ----------------- | --------------------------------------- |
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 | | 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 | | 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
| 🈶 **中文優化** | 內建中日韓字型與 OCR告別亂碼 | | 🈶 **中文優化** | 內建中日韓字型與 OCR告別亂碼 |
| 🌐 **65 種語言** | 跨國團隊無障礙使用 | | 🌐 **65 種語言** | 跨國團隊無障礙使用 |
| 📊 **PDF 翻譯** | 數學公式完整保留PDFMathTranslate | | 📊 **PDF 翻譯** | PDFMathTranslate + BabelDOC 雙引擎 |
| 📄 **PDF 轉 MD** | MinerU 智能擷取(保留表格、公式、圖片) |
--- ---
@ -26,25 +28,42 @@
完整文件請參閱 **[文件中心](docs/README.md)** 完整文件請參閱 **[文件中心](docs/README.md)**
| 分類 | 連結 | | 分類 | 連結 |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | | ----------- | -------------------------------------------------------------------------------------------------------- |
| 🚀 快速入門 | [概覽](docs/getting-started/overview.md) · [快速開始](docs/getting-started/quick-start.md) · [FAQ](docs/getting-started/faq.md) | | 🚀 快速入門 | [概覽](docs/快速入門/概覽.md) · [快速開始](docs/快速入門/快速開始.md) · [FAQ](docs/快速入門/常見問題.md) |
| 🐳 部署指南 | [Docker](docs/deployment/docker.md) · [反向代理](docs/deployment/reverse-proxy.md) | | 🐳 部署指南 | [Docker](docs/部署指南/Docker.md) · [反向代理](docs/部署指南/反向代理.md) |
| ⚙️ 配置設定 | [環境變數](docs/configuration/environment-variables.md) · [安全性](docs/configuration/security.md) | | ⚙️ 配置設定 | [環境變數](docs/配置設定/環境變數.md) · [安全性](docs/配置設定/安全性.md) |
| 🔌 功能說明 | [轉換器](docs/features/converters.md) · [OCR](docs/features/ocr.md) · [翻譯](docs/features/translation.md) | | 🔌 功能說明 | [轉換器](docs/功能說明/轉換器.md) · [OCR](docs/功能說明/OCR.md) · [翻譯](docs/功能說明/翻譯.md) |
| 🔗 API | [API 總覽](docs/api/overview.md) · [端點說明](docs/api/endpoints.md) | | 🔗 API | [API 總覽](docs/API/總覽.md) · [端點說明](docs/API/端點.md) |
| 👩‍💻 開發 | [專案結構](docs/development/project-structure.md) · [貢獻指南](docs/development/contribution.md) | | 👩‍💻 開發 | [專案結構](docs/開發指南/專案結構.md) · [貢獻指南](docs/開發指南/貢獻指南.md) |
--- ---
## 🚀 快速開始 ## 🚀 快速開始
### Docker Compose推薦 ### Docker Run
```bash ```bash
# 1. 建立專案資料夾 mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
mkdir -p ~/convertx-cn && 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' cat > docker-compose.yml << 'EOF'
services: services:
convertx: convertx:
@ -57,31 +76,16 @@ services:
- ./data:/app/data - ./data:/app/data
environment: environment:
- TZ=Asia/Taipei - TZ=Asia/Taipei
- JWT_SECRET=請更換為長且隨機的字串至少32字元 - JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0
EOF EOF
# 3. 啟動
docker compose up -d docker compose up -d
``` ```
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
開啟瀏覽器:`http://localhost:3000` 開啟瀏覽器:`http://localhost:3000`
> 📖 詳細說明請參閱 [快速開始](docs/getting-started/quick-start.md) > 📖 詳細說明請參閱 [快速開始](docs/快速入門/快速開始.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
```
--- ---
@ -94,7 +98,7 @@ docker run -d \
| 帳號 | admin@example.com | | 帳號 | admin@example.com |
| 密碼 | admin | | 密碼 | admin |
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案 > ⚠️ 示範站僅供測試,請勿上傳敏感檔案,會定期清理資料。
--- ---
@ -106,14 +110,14 @@ docker run -d \
| 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 | | 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 |
| 重啟後被登出 | 設定固定的 `JWT_SECRET` | | 重啟後被登出 | 設定固定的 `JWT_SECRET` |
更多問題 → [FAQ](docs/getting-started/faq.md) 更多問題 → [FAQ](docs/快速入門/常見問題.md)
--- ---
## 📦 支援格式 ## 📦 支援格式
| 轉換器 | 用途 | 格式數 | | 轉換器 | 用途 | 格式數 |
| ---------------- | -------- | ------ | | ---------------- | --------------- | ------ |
| FFmpeg | 影音 | 400+ | | FFmpeg | 影音 | 400+ |
| ImageMagick | 圖片 | 200+ | | ImageMagick | 圖片 | 200+ |
| LibreOffice | 文件 | 60+ | | LibreOffice | 文件 | 60+ |
@ -121,8 +125,10 @@ docker run -d \
| Calibre | 電子書 | 40+ | | Calibre | 電子書 | 40+ |
| Inkscape | 向量圖 | 20+ | | Inkscape | 向量圖 | 20+ |
| PDFMathTranslate | PDF 翻譯 | 15+ | | 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 ## 📄 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 ## 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 規格文件](../../api-server/docs/API_SPEC.md)
- [架構說明](../../api-server/docs/ARCHITECTURE.md) - [架構說明](../../api-server/docs/ARCHITECTURE.md)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@
> >
> 本文件內容已整合至新的文件結構,請參閱: > 本文件內容已整合至新的文件結構,請參閱:
> >
> - 📦 [Docker 部署指南](deployment/docker.md) > - 📦 [Docker 部署指南](部署指南/Docker.md)
> - 🔧 [反向代理設定](deployment/reverse-proxy.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) > - 📖 [概覽](快速入門/概覽.md)
> - 🚀 [快速開始](getting-started/quick-start.md) > - 🚀 [快速開始](快速入門/快速開始.md)
> - ❓ [常見問題](getting-started/faq.md) > - ❓ [常見問題](快速入門/常見問題.md)
> >
> 此文件將在未來版本中移除。 > 此文件將在未來版本中移除。

View file

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

View file

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

View file

@ -1,6 +1,10 @@
# OCR 功能 # 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 等) 1. 上傳圖片PNG, JPG, TIFF 等)
2. 選擇目標格式 `txt``pdf`(可搜尋) 2. 選擇目標格式 `txt``pdf`(可搜尋)
3. 進行轉換 3. 進行轉換
### PDF → 可搜尋 PDF ### 掃描版 PDF 翻譯(自動處理)
1. 上傳掃描版 PDF 當使用 PDFMathTranslate 或 BabelDOC 翻譯 PDF 時:
2. 選擇 OCR 處理
3. 獲得可搜尋的 PDF 1. 系統會**自動偵測**是否為掃描版 PDF無文字層
2. 若為掃描版,系統會**自動執行 OCR** 加入文字層
3. 翻譯引擎使用 OCR 處理後的 PDF 進行翻譯
4. 使用者無需手動操作,全程自動完成
--- ---
## 支援的輸入格式 ## 支援的輸入格式
- **點陣圖**PNG, JPG, JPEG, TIFF, BMP, GIF - **點陣圖**PNG, JPG, JPEG, TIFF, BMP, GIF
- **文件**PDF掃描版
- **其他**WebP, PNM, PBM - **其他**WebP, PNM, PBM
--- ---
@ -78,14 +109,18 @@ Tesseract 可同時辨識多種語言,但準確度可能下降。
### 方法一:自訂 Dockerfile ### 方法一:自訂 Dockerfile
> 💡 在 `Dockerfile.full` 中取消註解以下內容
```dockerfile ```dockerfile
# 在 Dockerfile.full 中取消註解
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-spa \ # 西班牙文 tesseract-ocr-spa \
tesseract-ocr-ita \ # 義大利文 tesseract-ocr-ita \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
``` ```
- `tesseract-ocr-spa` — 西班牙文
- `tesseract-ocr-ita` — 義大利文
### 方法二:掛載語言包 ### 方法二:掛載語言包
```yaml ```yaml
@ -112,6 +147,6 @@ volumes:
## 相關文件 ## 相關文件
- [支援的轉換器](converters.md) - [支援的轉換器](轉換器.md)
- [翻譯功能](translation.md) - [翻譯功能](翻譯.md)
- [Docker 部署](../deployment/docker.md) - [Docker 部署](../部署指南/Docker.md)

View file

@ -128,5 +128,5 @@ http://localhost:3000/?lang=ja
## 相關文件 ## 相關文件
- [貢獻指南](../development/contribution.md) - [貢獻指南](../開發指南/貢獻指南.md)
- [專案結構](../development/project-structure.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: 支援哪些格式? ### Q: 支援哪些格式?
1000+ 種格式,詳見 [支援的轉換器](../features/converters.md) 1000+ 種格式,詳見 [支援的轉換器](../功能說明/轉換器.md)
### Q: 轉換失敗怎麼辦? ### Q: 轉換失敗怎麼辦?
@ -169,11 +169,11 @@ client_max_body_size 500M;
### Q: 如何使用反向代理? ### Q: 如何使用反向代理?
詳見 [反向代理設定](../deployment/reverse-proxy.md) 詳見 [反向代理設定](../部署指南/反向代理.md)
### Q: 如何啟用硬體加速? ### Q: 如何啟用硬體加速?
詳見 [進階配置 - 硬體加速](../deployment/docker.md#硬體加速) 詳見 [進階配置 - 硬體加速](../部署指南/Docker.md#硬體加速)
### Q: 如何啟用 API Server ### Q: 如何啟用 API Server
@ -181,7 +181,7 @@ client_max_body_size 500M;
docker compose --profile api up -d docker compose --profile api up -d
``` ```
詳見 [API 文件](../api/overview.md) 詳見 [API 文件](../API/總覽.md)
--- ---

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -97,21 +97,29 @@ docker run -d \
**重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。 **重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。
```bash **Linux / macOS**
# Linux / macOS
mkdir -p ~/convertx-cn/data
# Windows PowerShell ```bash
mkdir -p ~/convertx-cn/data
```
**Windows PowerShell**
```powershell
mkdir C:\convertx-cn\data 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 tar -xzvf convertx-backup-20260120.tar.gz
``` ```
@ -199,18 +207,23 @@ services:
## 版本更新 ## 版本更新
```bash **1. 拉取最新版本:**
# 拉取最新版本
docker pull convertx/convertx-cn:latest
# 停止並移除舊容器 ```bash
docker pull convertx/convertx-cn:latest
```
**2. 停止並移除舊容器:**
```bash
docker stop convertx-cn docker stop convertx-cn
docker rm convertx-cn docker rm convertx-cn
```
# 重新啟動(使用相同的參數) **3. 重新啟動(使用相同的參數):**
docker run -d \
--name convertx-cn \ ```bash
# ... 其他參數 docker run -d --name convertx-cn ...
``` ```
或使用 Docker Compose 或使用 Docker Compose
@ -228,7 +241,12 @@ docker compose up -d
```bash ```bash
docker logs convertx-cn 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) - [Docker Compose 詳解](docker-compose.md)
- [反向代理設定](reverse-proxy.md) - [反向代理設定](反向代理.md)
- [環境變數設定](../configuration/environment-variables.md) - [環境變數設定](../配置設定/環境變數.md)

View file

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

View file

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

View file

@ -24,12 +24,12 @@ docker compose up -d
### 更新到指定版本 ### 更新到指定版本
修改 `docker-compose.yml` 修改 `docker-compose.yml`(指定要更新的版本號)
```yaml ```yaml
services: services:
convertx: 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 ```yaml
services: services:
convertx: convertx:
image: convertx/convertx-cn:v0.1.8 # 舊版本 image: convertx/convertx-cn:v0.1.8
``` ```
```bash ```bash
@ -120,6 +120,8 @@ docker rmi convertx/convertx-cn:v0.1.7
可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器: 可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器:
> 💡 `--interval 86400` — 每 24 小時檢查一次更新
```yaml ```yaml
services: services:
convertx: convertx:
@ -130,7 +132,7 @@ services:
image: containrrr/watchtower image: containrrr/watchtower
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /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 ```yaml
environment: environment:
- TRUST_PROXY=true # 信任 X-Forwarded-* headers - TRUST_PROXY=true
- HTTP_ALLOWED=false # Proxy 已處理 HTTPS - HTTP_ALLOWED=false
``` ```
--- ---

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,6 +31,7 @@ import {
properties as propertiesPDFMathTranslate, properties as propertiesPDFMathTranslate,
} from "./pdfmathtranslate"; } from "./pdfmathtranslate";
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc"; 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 // 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, properties: propertiesBabelDoc,
converter: convertBabelDoc, converter: convertBabelDoc,
}, },
OCRmyPDF: {
properties: propertiesOcrMyPdf,
converter: convertOcrMyPdf,
},
}; };
function chunks<T>(arr: T[], size: number): T[][] { 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 { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, copyFileSync } from "node:fs";
import { join, basename, dirname } from "node:path"; import { join, basename, dirname } from "node:path";
import { getArchiveFileName } from "../transfer"; import { getArchiveFileName } from "../transfer";
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
import type { ExecFileFn } from "./types"; import type { ExecFileFn } from "./types";
// 翻譯服務優先順序(自動 fallback // 翻譯服務優先順序(自動 fallback
@ -339,7 +340,19 @@ export async function convert(
_options?: unknown, _options?: unknown,
execFile: ExecFileFn = execFileOriginal, execFile: ExecFileFn = execFileOriginal,
): Promise<string> { ): Promise<string> {
let ocrTempFile: string | undefined;
try { 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 可能會自動下載) // 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
checkModelsExist(); checkModelsExist();
@ -361,8 +374,8 @@ export async function convert(
const archiveDir = join(tempDir, "archive"); const archiveDir = join(tempDir, "archive");
mkdirSync(archiveDir, { recursive: true }); mkdirSync(archiveDir, { recursive: true });
// 5. 執行 pdf2zh 翻譯 // 5. 執行 pdf2zh 翻譯(使用 OCR 處理後的 PDF
const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile); const { monoPath, dualPath } = await runPdf2zh(inputPdf, tempDir, targetLang, execFile);
// 6. 複製翻譯後的檔案到封裝目錄 // 6. 複製翻譯後的檔案到封裝目錄
// PDFMathTranslate 輸出: // PDFMathTranslate 輸出:
@ -440,8 +453,13 @@ export async function convert(
// 9. 清理臨時目錄 // 9. 清理臨時目錄
removeDir(tempDir); removeDir(tempDir);
// 10. 清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
return "Done"; return "Done";
} catch (error) { } catch (error) {
// 確保清理 OCR 暫存檔案
cleanupOcrTempFile(ocrTempFile);
throw new Error(`PDFMathTranslate error: ${error}`); 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[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; babeldocCalled = true;
babeldocArgs = args; babeldocArgs = args;
@ -131,7 +134,9 @@ describe("BabelDOC converter - English translation", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; babeldocArgs = args;
const outputIndex = args.indexOf("-o"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -181,7 +186,9 @@ describe("BabelDOC converter - Traditional Chinese", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; babeldocArgs = args;
const outputIndex = args.indexOf("-o"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -231,7 +238,9 @@ describe("BabelDOC converter - Output structure", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {
const outputPath = args[outputIndex + 1]; const outputPath = args[outputIndex + 1];
@ -267,7 +276,9 @@ describe("BabelDOC converter - Output structure", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {
const outputPath = args[outputIndex + 1]; const outputPath = args[outputIndex + 1];
@ -317,7 +328,9 @@ describe("BabelDOC converter - Error handling", () => {
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; const error = new Error("Translation failed") as ExecFileException;
error.code = 1; error.code = 1;
callback(error, "", "babeldoc: Translation error"); callback(error, "", "babeldoc: Translation error");
@ -337,7 +350,9 @@ describe("BabelDOC converter - Error handling", () => {
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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 // Don't create output file, simulating failed translation
callback(null, "Complete", ""); callback(null, "Complete", "");
} }
@ -376,7 +391,9 @@ describe("BabelDOC converter - Markdown output format", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; babeldocArgs = args;
const outputIndex = args.indexOf("-o"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {
@ -428,7 +445,9 @@ describe("BabelDOC converter - HTML output format", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; babeldocArgs = args;
const outputIndex = args.indexOf("-o"); const outputIndex = args.indexOf("-o");
if (outputIndex !== -1 && args[outputIndex + 1]) { if (outputIndex !== -1 && args[outputIndex + 1]) {

View file

@ -55,7 +55,7 @@ describe("PDFMathTranslate converter - Chinese translation", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; pdf2zhCalled = true;
pdf2zhArgs = args; pdf2zhArgs = args;
@ -116,7 +116,7 @@ describe("PDFMathTranslate converter - English translation", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o"); const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -165,7 +165,7 @@ describe("PDFMathTranslate converter - Japanese translation", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o"); const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -214,7 +214,7 @@ describe("PDFMathTranslate converter - Traditional Chinese", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; pdf2zhArgs = args;
const outputDirIndex = args.indexOf("-o"); const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
@ -264,7 +264,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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"); const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
const outputDir = args[outputDirIndex + 1]; const outputDir = args[outputDirIndex + 1];
@ -308,7 +308,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
args: string[], args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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"); const outputDirIndex = args.indexOf("-o");
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) { if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
const outputDir = args[outputDirIndex + 1]; const outputDir = args[outputDirIndex + 1];
@ -362,7 +362,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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; const error = new Error("Translation failed") as ExecFileException;
callback(error, "", "Error: Translation service unavailable"); callback(error, "", "Error: Translation service unavailable");
} }
@ -381,7 +381,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
_args: string[], _args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void, 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 // Don't create any output files
callback(null, "Complete", ""); callback(null, "Complete", "");
} else if (cmd === "tar") { } else if (cmd === "tar") {