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:
parent
f24eec070c
commit
a06df23b1d
53 changed files with 1427 additions and 675 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,5 +1,29 @@
|
|||
# Changelog
|
||||
|
||||
## [0.1.14](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.14) (2026-01-23)
|
||||
|
||||
OCR 功能強化版本,新增 OCRmyPDF 轉換引擎。
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- **OCRmyPDF 轉換引擎**:新增獨立的 OCR 轉換引擎,支援將掃描版 PDF 轉換為可搜尋 PDF
|
||||
- 支援 7 種語言:英文、繁體中文、簡體中文、日文、韓文、德文、法文
|
||||
- 與 PDFMathTranslate 風格一致的 UI 格式(`pdf-en`、`pdf-zh-TW` 等)
|
||||
- 自動偵測頁面方向並旋轉
|
||||
- 自動校正傾斜
|
||||
- 跳過已有文字層的頁面
|
||||
- 詳細的5階段處理進度輸出
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- **Dockerfile**:安裝 ocrmypdf 與 Tesseract OCR 語言包(階段 9/11)
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- 更新 OCR 功能文件,說明 OCRmyPDF 轉換引擎用法
|
||||
|
||||
---
|
||||
|
||||
## [0.1.13](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.13) (2026-01-23)
|
||||
|
||||
工作流程優化與測試修復版本。
|
||||
|
|
|
|||
120
Dockerfile
120
Dockerfile
|
|
@ -116,15 +116,24 @@ RUN echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries \
|
|||
&& echo 'DPkg::Lock::Timeout "120";' >> /etc/apt/apt.conf.d/80-retries
|
||||
|
||||
# 階段 1:基礎系統工具
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 1/11:安裝基礎系統工具" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
locales \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 1/11 完成:基礎系統工具已安裝"
|
||||
|
||||
# 階段 2:核心轉換工具(小型)
|
||||
# 注意:dasel 和 resvg 在 bookworm 中不存在,後續用二進位檔案安裝
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 2/11:安裝核心轉換工具" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
assimp-utils \
|
||||
dcraw \
|
||||
dvisvgm \
|
||||
|
|
@ -133,64 +142,95 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
|||
mupdf-tools \
|
||||
poppler-utils \
|
||||
potrace \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 2/11 完成:核心轉換工具已安裝"
|
||||
|
||||
# 階段 2.1:安裝 dasel(從 GitHub 下載二進位檔案)
|
||||
RUN ARCH=$(uname -m) && \
|
||||
RUN echo "" && \
|
||||
echo " 🔧 階段 2.1:安裝 dasel..." && \
|
||||
ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "aarch64" ]; then \
|
||||
DASEL_ARCH="linux_arm64"; \
|
||||
else \
|
||||
DASEL_ARCH="linux_amd64"; \
|
||||
fi && \
|
||||
curl -sSLf "https://github.com/TomWright/dasel/releases/download/v2.8.1/dasel_${DASEL_ARCH}" -o /usr/local/bin/dasel && \
|
||||
chmod +x /usr/local/bin/dasel
|
||||
chmod +x /usr/local/bin/dasel && \
|
||||
echo " ✅ dasel 安裝完成"
|
||||
|
||||
# 階段 2.2:安裝 resvg(從 GitHub 下載二進位檔案)
|
||||
# 注意:resvg 官方只提供 x86_64 版本,ARM64 需從源碼編譯或跳過
|
||||
RUN ARCH=$(uname -m) && \
|
||||
RUN echo "" && \
|
||||
echo " 🔧 階段 2.2:安裝 resvg..." && \
|
||||
ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "aarch64" ]; then \
|
||||
echo "⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝(可改用 ImageMagick 或 Inkscape 替代)"; \
|
||||
echo " ⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝"; \
|
||||
else \
|
||||
curl -sSLf "https://github.com/linebender/resvg/releases/download/v0.44.0/resvg-linux-x86_64.tar.gz" -o /tmp/resvg.tar.gz && \
|
||||
tar -xzf /tmp/resvg.tar.gz -C /tmp/ && \
|
||||
mv /tmp/resvg /usr/local/bin/resvg && \
|
||||
chmod +x /usr/local/bin/resvg && \
|
||||
rm -rf /tmp/resvg.tar.gz; \
|
||||
rm -rf /tmp/resvg.tar.gz && \
|
||||
echo " ✅ resvg 安裝完成"; \
|
||||
fi
|
||||
|
||||
# 階段 3:影音處理工具
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 3/11:安裝影音處理工具" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
libavcodec-extra \
|
||||
libva2 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 3/11 完成:影音處理工具已安裝(ffmpeg)"
|
||||
|
||||
# 階段 4:圖像處理工具
|
||||
# 注意:bookworm 使用 imagemagick(版本 6),trixie 才有 imagemagick-7
|
||||
# 注意:Inkscape 需要 xvfb 在無 DISPLAY 環境下執行某些操作(如 PNG 轉 SVG)
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 4/11:安裝圖像處理工具" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
imagemagick \
|
||||
inkscape \
|
||||
libheif-examples \
|
||||
libjxl-tools \
|
||||
libvips-tools \
|
||||
xvfb \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 4/11 完成:圖像處理工具已安裝(ImageMagick, Inkscape, VIPS)"
|
||||
|
||||
# 階段 5:文件處理工具
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 5/11:安裝文件處理工具" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
calibre \
|
||||
libemail-outlook-message-perl \
|
||||
pandoc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 5/11 完成:文件處理工具已安裝(Calibre, Pandoc)"
|
||||
|
||||
# 階段 6:LibreOffice(最大的套件,單獨安裝)
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 6/11:安裝 LibreOffice(較大,需要數分鐘)" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
libreoffice \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 6/11 完成:LibreOffice 已安裝"
|
||||
|
||||
# 階段 7:TexLive 基礎
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 7/11:安裝 TexLive 基礎(較大,需要數分鐘)" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
texlive-base \
|
||||
texlive-latex-base \
|
||||
texlive-latex-recommended \
|
||||
|
|
@ -198,19 +238,29 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
|||
texlive-xetex \
|
||||
latexmk \
|
||||
lmodern \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 7/11 完成:TexLive 基礎已安裝"
|
||||
|
||||
# 階段 8:TexLive 語言包
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 8/11:安裝 TexLive 語言包(CJK + 歐語)" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
texlive-lang-cjk \
|
||||
texlive-lang-german \
|
||||
texlive-lang-french \
|
||||
texlive-lang-arabic \
|
||||
texlive-lang-other \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 8/11 完成:TexLive 語言包已安裝"
|
||||
|
||||
# 階段 9:OCR 支援
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 9/11:安裝 OCR 支援(Tesseract + ocrmypdf)" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
tesseract-ocr-chi-tra \
|
||||
|
|
@ -219,25 +269,41 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
|||
tesseract-ocr-kor \
|
||||
tesseract-ocr-deu \
|
||||
tesseract-ocr-fra \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ocrmypdf \
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 9/11 完成:OCR 支援已安裝(7 種語言)"
|
||||
|
||||
# 階段 10:字型
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 10/11:安裝字型(Noto CJK + Liberation)" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-core \
|
||||
fonts-noto-color-emoji \
|
||||
fonts-liberation \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 10/11 完成:字型已安裝"
|
||||
|
||||
# 階段 11:Python 依賴
|
||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
RUN echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "📦 階段 11/11:安裝 Python 依賴" && \
|
||||
echo "========================================" && \
|
||||
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-numpy \
|
||||
python3-tinycss2 \
|
||||
python3-opencv \
|
||||
pipx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
echo "✅ 階段 11/11 完成:Python 依賴已安裝" && \
|
||||
echo "" && \
|
||||
echo "========================================" && \
|
||||
echo "✅ 所有 APT 套件安裝完成!" && \
|
||||
echo "========================================"
|
||||
|
||||
# ==============================================================================
|
||||
# 🔥 階段 12-UNIFIED:Python 工具安裝 + 模型下載(單一 RUN 原則)
|
||||
|
|
|
|||
426
LICENSE
426
LICENSE
|
|
@ -1,178 +1,179 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
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
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
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
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
which gives you legal permission to copy, distribute and/or modify the
|
||||
software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
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
|
||||
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
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This
|
||||
is a different license, not a version of the Affero GPL, but Affero
|
||||
has released a new version of the Affero GPL which permits relicensing
|
||||
under this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
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
|
||||
works, such as semiconductor masks.
|
||||
"Copyright" also means copyright-like laws that apply to other kinds
|
||||
of works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
To "modify" a work means to copy from or adapt all or part of the
|
||||
work in a fashion requiring copyright permission, other than the making
|
||||
of an exact copy. The resulting work is called a "modified version"
|
||||
of the earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
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
|
||||
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
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
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
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
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
|
||||
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
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
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
|
||||
interfaces specified for a particular programming language, one that
|
||||
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
|
||||
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
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(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.
|
||||
|
||||
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
|
||||
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
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
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
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
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
|
||||
is effected by exercising rights under this License with respect to
|
||||
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
|
||||
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
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
|
|
@ -220,19 +221,19 @@ terms of section 4, provided that you also meet all of these conditions:
|
|||
interfaces that do not display Appropriate Legal Notices, your
|
||||
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,
|
||||
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
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
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
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
|
@ -278,77 +279,77 @@ in one of these ways:
|
|||
Source of the work are being offered to the general public at no
|
||||
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
|
||||
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,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
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
|
||||
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
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
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
|
||||
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
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
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
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
the User Product in which it has been modified or installed. Access to
|
||||
a network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
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
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
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.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
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
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders
|
||||
of that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
|
@ -373,74 +374,74 @@ that material) supplement the terms of this License with terms:
|
|||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
All other non-permissive additional terms are considered "further
|
||||
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
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
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
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
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;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
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)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
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
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
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
|
||||
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
|
||||
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
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
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
|
||||
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,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
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
|
||||
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.
|
||||
|
||||
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 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
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
|
|
@ -448,43 +449,43 @@ give under the previous paragraph, plus a right to possession of the
|
|||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
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
|
||||
License of the Program or a work on which the Program is based. The
|
||||
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
|
||||
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
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
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
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
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
|
||||
(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
|
||||
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
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
|
|
@ -492,13 +493,13 @@ then you must either (1) cause the Corresponding Source to be so
|
|||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
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
|
||||
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
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
|
|
@ -506,10 +507,10 @@ or convey a specific copy of the covered work, then the patent license
|
|||
you grant is automatically extended to all recipients of the covered
|
||||
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
|
||||
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
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
|
|
@ -521,95 +522,86 @@ for and in connection with specific products or compilations that
|
|||
contain the covered work, unless you entered into that arrangement,
|
||||
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
|
||||
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
|
||||
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
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not convey it at all. For example, if you agree to terms that
|
||||
obligate you to collect a royalty for further conveying from those to
|
||||
whom you convey the Program, the only way you could satisfy both those
|
||||
terms and this License would be to refrain entirely from conveying the
|
||||
Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
13. Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Affero General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero
|
||||
General Public License "or any later version" applies to it, you have
|
||||
the option of following the terms and conditions either of that
|
||||
numbered version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number
|
||||
of the GNU Affero General Public License, you may choose any version
|
||||
ever published by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that
|
||||
proxy's public statement of acceptance of a version permanently
|
||||
authorizes you to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
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
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
|
||||
OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
|
||||
THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
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,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
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.
|
||||
|
||||
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/>.
|
||||
|
|
|
|||
110
README.md
110
README.md
|
|
@ -6,18 +6,20 @@
|
|||
|
||||
[](https://hub.docker.com/r/convertx/convertx-cn)
|
||||
[](https://github.com/pi-docket/ConvertX-CN/releases)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
## 為什麼選擇 ConvertX-CN?
|
||||
|
||||
| 特色 | 說明 |
|
||||
| ----------------- | ------------------------------------ |
|
||||
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
|
||||
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
|
||||
| 🈶 **中文優化** | 內建中日韓字型與 OCR,告別亂碼 |
|
||||
| 🌐 **65 種語言** | 跨國團隊無障礙使用 |
|
||||
| 📊 **PDF 翻譯** | 數學公式完整保留(PDFMathTranslate) |
|
||||
| 特色 | 說明 |
|
||||
| ----------------- | --------------------------------------- |
|
||||
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
|
||||
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
|
||||
| 🈶 **中文優化** | 內建中日韓字型與 OCR,告別亂碼 |
|
||||
| 🌐 **65 種語言** | 跨國團隊無障礙使用 |
|
||||
| 📊 **PDF 翻譯** | PDFMathTranslate + BabelDOC 雙引擎 |
|
||||
| 📄 **PDF 轉 MD** | MinerU 智能擷取(保留表格、公式、圖片) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -25,26 +27,43 @@
|
|||
|
||||
完整文件請參閱 **[文件中心](docs/README.md)**
|
||||
|
||||
| 分類 | 連結 |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 🚀 快速入門 | [概覽](docs/getting-started/overview.md) · [快速開始](docs/getting-started/quick-start.md) · [FAQ](docs/getting-started/faq.md) |
|
||||
| 🐳 部署指南 | [Docker](docs/deployment/docker.md) · [反向代理](docs/deployment/reverse-proxy.md) |
|
||||
| ⚙️ 配置設定 | [環境變數](docs/configuration/environment-variables.md) · [安全性](docs/configuration/security.md) |
|
||||
| 🔌 功能說明 | [轉換器](docs/features/converters.md) · [OCR](docs/features/ocr.md) · [翻譯](docs/features/translation.md) |
|
||||
| 🔗 API | [API 總覽](docs/api/overview.md) · [端點說明](docs/api/endpoints.md) |
|
||||
| 👩💻 開發 | [專案結構](docs/development/project-structure.md) · [貢獻指南](docs/development/contribution.md) |
|
||||
| 分類 | 連結 |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| 🚀 快速入門 | [概覽](docs/快速入門/概覽.md) · [快速開始](docs/快速入門/快速開始.md) · [FAQ](docs/快速入門/常見問題.md) |
|
||||
| 🐳 部署指南 | [Docker](docs/部署指南/Docker.md) · [反向代理](docs/部署指南/反向代理.md) |
|
||||
| ⚙️ 配置設定 | [環境變數](docs/配置設定/環境變數.md) · [安全性](docs/配置設定/安全性.md) |
|
||||
| 🔌 功能說明 | [轉換器](docs/功能說明/轉換器.md) · [OCR](docs/功能說明/OCR.md) · [翻譯](docs/功能說明/翻譯.md) |
|
||||
| 🔗 API | [API 總覽](docs/API/總覽.md) · [端點說明](docs/API/端點.md) |
|
||||
| 👩💻 開發 | [專案結構](docs/開發指南/專案結構.md) · [貢獻指南](docs/開發指南/貢獻指南.md) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速開始
|
||||
|
||||
### Docker Compose(推薦)
|
||||
### Docker Run
|
||||
|
||||
```bash
|
||||
# 1. 建立專案資料夾
|
||||
mkdir -p ~/convertx-cn && cd ~/convertx-cn
|
||||
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
|
||||
docker run -d \
|
||||
--name convertx-cn \
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-v ./data:/app/data \
|
||||
-e TZ=Asia/Taipei \
|
||||
-e JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0 \
|
||||
convertx/convertx-cn:latest
|
||||
```
|
||||
|
||||
# 2. 建立 docker-compose.yml
|
||||
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
|
||||
|
||||
開啟瀏覽器:`http://localhost:3000`
|
||||
|
||||
### Docker Compose(推薦)
|
||||
|
||||
> 💡 以下命令會自動建立 `~/convertx-cn/data` 資料夾、產生 `docker-compose.yml` 並啟動服務
|
||||
|
||||
```bash
|
||||
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
services:
|
||||
convertx:
|
||||
|
|
@ -57,31 +76,16 @@ services:
|
|||
- ./data:/app/data
|
||||
environment:
|
||||
- TZ=Asia/Taipei
|
||||
- JWT_SECRET=請更換為長且隨機的字串至少32字元
|
||||
- JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0
|
||||
EOF
|
||||
|
||||
# 3. 啟動
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
|
||||
|
||||
開啟瀏覽器:`http://localhost:3000`
|
||||
|
||||
> 📖 詳細說明請參閱 [快速開始](docs/getting-started/quick-start.md)
|
||||
|
||||
### Docker Run
|
||||
|
||||
```bash
|
||||
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn
|
||||
|
||||
docker run -d \
|
||||
--name convertx-cn \
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-v ./data:/app/data \
|
||||
-e TZ=Asia/Taipei \
|
||||
-e JWT_SECRET=請更換為長且隨機的字串 \
|
||||
convertx/convertx-cn:latest
|
||||
```
|
||||
> 📖 詳細說明請參閱 [快速開始](docs/快速入門/快速開始.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -94,7 +98,7 @@ docker run -d \
|
|||
| 帳號 | admin@example.com |
|
||||
| 密碼 | admin |
|
||||
|
||||
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案
|
||||
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案,會定期清理資料。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -106,23 +110,25 @@ docker run -d \
|
|||
| 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 |
|
||||
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
||||
|
||||
更多問題 → [FAQ](docs/getting-started/faq.md)
|
||||
更多問題 → [FAQ](docs/快速入門/常見問題.md)
|
||||
|
||||
---
|
||||
|
||||
## 📦 支援格式
|
||||
|
||||
| 轉換器 | 用途 | 格式數 |
|
||||
| ---------------- | -------- | ------ |
|
||||
| FFmpeg | 影音 | 400+ |
|
||||
| ImageMagick | 圖片 | 200+ |
|
||||
| LibreOffice | 文件 | 60+ |
|
||||
| Pandoc | 文件 | 100+ |
|
||||
| Calibre | 電子書 | 40+ |
|
||||
| Inkscape | 向量圖 | 20+ |
|
||||
| PDFMathTranslate | PDF 翻譯 | 15+ |
|
||||
| 轉換器 | 用途 | 格式數 |
|
||||
| ---------------- | --------------- | ------ |
|
||||
| FFmpeg | 影音 | 400+ |
|
||||
| ImageMagick | 圖片 | 200+ |
|
||||
| LibreOffice | 文件 | 60+ |
|
||||
| Pandoc | 文件 | 100+ |
|
||||
| Calibre | 電子書 | 40+ |
|
||||
| Inkscape | 向量圖 | 20+ |
|
||||
| PDFMathTranslate | PDF 翻譯 | 15+ |
|
||||
| BabelDOC | PDF 翻譯/轉換 | 15+ |
|
||||
| MinerU | PDF 轉 Markdown | 10+ |
|
||||
|
||||
完整列表 → [轉換器文件](docs/features/converters.md)
|
||||
完整列表 → [轉換器文件](docs/功能說明/轉換器.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -144,4 +150,6 @@ docker compose up -d
|
|||
|
||||
## 📄 License
|
||||
|
||||
[MIT](LICENSE) | 基於 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
|
||||
This project is licensed under the **[GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE)**.
|
||||
|
||||
Based on [C4illin/ConvertX](https://github.com/C4illin/ConvertX).
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ Only the latest release is supported
|
|||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.
|
||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/pi-docket/ConvertX-CN/security/advisories/new) tab.
|
||||
|
|
|
|||
|
|
@ -123,6 +123,6 @@ curl -X POST \
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [API 端點詳細說明](endpoints.md)
|
||||
- [API 端點詳細說明](端點.md)
|
||||
- [API 規格文件](../../api-server/docs/API_SPEC.md)
|
||||
- [架構說明](../../api-server/docs/ARCHITECTURE.md)
|
||||
156
docs/README.md
156
docs/README.md
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
剛開始使用?從這裡開始:
|
||||
|
||||
1. **[概覽](getting-started/overview.md)** — 了解 ConvertX-CN 是什麼
|
||||
2. **[快速開始](getting-started/quick-start.md)** — 5 分鐘內完成部署
|
||||
3. **[常見問題](getting-started/faq.md)** — 解決常見問題
|
||||
1. **[概覽](快速入門/概覽.md)** — 了解 ConvertX-CN 是什麼
|
||||
2. **[快速開始](快速入門/快速開始.md)** — 5 分鐘內完成部署
|
||||
3. **[常見問題](快速入門/常見問題.md)** — 解決常見問題
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -18,14 +18,14 @@
|
|||
|
||||
適合一般使用者:
|
||||
|
||||
| 文件 | 說明 |
|
||||
| ------------------------------------------ | -------------------- |
|
||||
| [快速開始](getting-started/quick-start.md) | 最快部署方式 |
|
||||
| [支援的轉換器](features/converters.md) | 所有可用的轉換格式 |
|
||||
| [OCR 功能](features/ocr.md) | 光學字元辨識 |
|
||||
| [翻譯功能](features/translation.md) | PDF 翻譯(保留公式) |
|
||||
| [多語言介面](features/i18n.md) | 切換介面語言 |
|
||||
| [常見問題](getting-started/faq.md) | FAQ |
|
||||
| 文件 | 說明 |
|
||||
| ---------------------------------- | -------------------- |
|
||||
| [快速開始](快速入門/快速開始.md) | 最快部署方式 |
|
||||
| [支援的轉換器](功能說明/轉換器.md) | 所有可用的轉換格式 |
|
||||
| [OCR 功能](功能說明/OCR.md) | 光學字元辨識 |
|
||||
| [翻譯功能](功能說明/翻譯.md) | PDF 翻譯(保留公式) |
|
||||
| [多語言介面](功能說明/多語言.md) | 切換介面語言 |
|
||||
| [常見問題](快速入門/常見問題.md) | FAQ |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -35,19 +35,19 @@
|
|||
|
||||
### 部署
|
||||
|
||||
| 文件 | 說明 |
|
||||
| --------------------------------------- | --------------------------- |
|
||||
| [Docker 部署](deployment/docker.md) | Docker Run & Docker Compose |
|
||||
| [反向代理](deployment/reverse-proxy.md) | Nginx / Traefik / Caddy |
|
||||
| [範例配置](samples/README.md) | 可直接使用的配置檔 |
|
||||
| 文件 | 說明 |
|
||||
| --------------------------------- | --------------------------- |
|
||||
| [Docker 部署](部署指南/Docker.md) | Docker Run & Docker Compose |
|
||||
| [反向代理](部署指南/反向代理.md) | Nginx / Traefik / Caddy |
|
||||
| [範例配置](範例/README.md) | 可直接使用的配置檔 |
|
||||
|
||||
### 配置
|
||||
|
||||
| 文件 | 說明 |
|
||||
| -------------------------------------------------- | ------------------ |
|
||||
| [環境變數](configuration/environment-variables.md) | 所有可用設定 |
|
||||
| [安全性設定](configuration/security.md) | HTTPS、認證、防護 |
|
||||
| [清理與限制](configuration/limits-and-cleanup.md) | 自動清理、資源限制 |
|
||||
| 文件 | 說明 |
|
||||
| ------------------------------------ | ------------------ |
|
||||
| [環境變數](配置設定/環境變數.md) | 所有可用設定 |
|
||||
| [安全性設定](配置設定/安全性.md) | HTTPS、認證、防護 |
|
||||
| [清理與限制](配置設定/清理與限制.md) | 自動清理、資源限制 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -57,26 +57,26 @@
|
|||
|
||||
### 開發
|
||||
|
||||
| 文件 | 說明 |
|
||||
| -------------------------------------------- | -------------- |
|
||||
| [專案結構](development/project-structure.md) | 程式碼結構說明 |
|
||||
| [本地開發](development/local-development.md) | 開發環境設定 |
|
||||
| [貢獻指南](development/contribution.md) | 如何參與專案 |
|
||||
| 文件 | 說明 |
|
||||
| -------------------------------- | -------------- |
|
||||
| [專案結構](開發指南/專案結構.md) | 程式碼結構說明 |
|
||||
| [本地開發](開發指南/本地開發.md) | 開發環境設定 |
|
||||
| [貢獻指南](開發指南/貢獻指南.md) | 如何參與專案 |
|
||||
|
||||
### API
|
||||
|
||||
| 文件 | 說明 |
|
||||
| ---------------------------- | ------------------ |
|
||||
| [API 總覽](api/overview.md) | REST & GraphQL API |
|
||||
| [API 端點](api/endpoints.md) | 詳細端點說明 |
|
||||
| 文件 | 說明 |
|
||||
| ----------------------- | ------------------ |
|
||||
| [API 總覽](API/總覽.md) | REST & GraphQL API |
|
||||
| [API 端點](API/端點.md) | 詳細端點說明 |
|
||||
|
||||
### 測試
|
||||
|
||||
| 文件 | 說明 |
|
||||
| ------------------------------------ | -------------- |
|
||||
| [測試策略](testing/test-strategy.md) | 測試類型與方法 |
|
||||
| [CI/CD](testing/ci-cd.md) | 持續整合設定 |
|
||||
| [E2E 測試](testing/e2e-tests.md) | 端對端測試 |
|
||||
| 文件 | 說明 |
|
||||
| ---------------------------- | -------------- |
|
||||
| [測試策略](測試/測試策略.md) | 測試類型與方法 |
|
||||
| [CI/CD](測試/CI-CD.md) | 持續整合設定 |
|
||||
| [E2E 測試](測試/E2E測試.md) | 端對端測試 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -85,38 +85,38 @@
|
|||
```
|
||||
docs/
|
||||
├── README.md ← 您在這裡
|
||||
├── getting-started/ # 快速入門
|
||||
│ ├── overview.md # 概覽
|
||||
│ ├── quick-start.md # 快速開始
|
||||
│ └── faq.md # 常見問題
|
||||
├── deployment/ # 部署指南
|
||||
│ ├── docker.md # Docker 部署
|
||||
│ └── reverse-proxy.md # 反向代理
|
||||
├── configuration/ # 配置設定
|
||||
│ ├── environment-variables.md # 環境變數
|
||||
│ ├── security.md # 安全性
|
||||
│ └── limits-and-cleanup.md # 清理與限制
|
||||
├── features/ # 功能說明
|
||||
│ ├── converters.md # 轉換器
|
||||
│ ├── translation.md # 翻譯
|
||||
│ ├── ocr.md # OCR
|
||||
│ └── i18n.md # 多語言
|
||||
├── api/ # API 文件
|
||||
│ ├── overview.md # API 總覽
|
||||
│ └── endpoints.md # API 端點
|
||||
├── testing/ # 測試
|
||||
│ ├── test-strategy.md # 測試策略
|
||||
│ ├── ci-cd.md # CI/CD
|
||||
│ └── e2e-tests.md # E2E 測試
|
||||
├── development/ # 開發指南
|
||||
│ ├── project-structure.md # 專案結構
|
||||
│ ├── local-development.md # 本地開發
|
||||
│ └── contribution.md # 貢獻指南
|
||||
└── samples/ # 範例檔案
|
||||
├── compose.minimal.yml # 最小配置
|
||||
├── compose.production.yml # 生產環境
|
||||
├── compose.with-traefik.yml # Traefik 整合
|
||||
└── nginx.example.conf # Nginx 設定
|
||||
├── 快速入門/
|
||||
│ ├── 概覽.md
|
||||
│ ├── 快速開始.md
|
||||
│ └── 常見問題.md
|
||||
├── 部署指南/
|
||||
│ ├── Docker.md
|
||||
│ └── 反向代理.md
|
||||
├── 配置設定/
|
||||
│ ├── 環境變數.md
|
||||
│ ├── 安全性.md
|
||||
│ └── 清理與限制.md
|
||||
├── 功能說明/
|
||||
│ ├── 轉換器.md
|
||||
│ ├── 翻譯.md
|
||||
│ ├── OCR.md
|
||||
│ └── 多語言.md
|
||||
├── API/
|
||||
│ ├── 總覽.md
|
||||
│ └── 端點.md
|
||||
├── 測試/
|
||||
│ ├── 測試策略.md
|
||||
│ ├── CI-CD.md
|
||||
│ └── E2E測試.md
|
||||
├── 開發指南/
|
||||
│ ├── 專案結構.md
|
||||
│ ├── 本地開發.md
|
||||
│ └── 貢獻指南.md
|
||||
└── 範例/
|
||||
├── compose.minimal.yml
|
||||
├── compose.production.yml
|
||||
├── compose.with-traefik.yml
|
||||
└── nginx.example.conf
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -135,23 +135,23 @@ docs/
|
|||
|
||||
### 我是新手
|
||||
|
||||
1. [概覽](getting-started/overview.md)
|
||||
2. [快速開始](getting-started/quick-start.md)
|
||||
3. [支援的轉換器](features/converters.md)
|
||||
1. [概覽](快速入門/概覽.md)
|
||||
2. [快速開始](快速入門/快速開始.md)
|
||||
3. [支援的轉換器](功能說明/轉換器.md)
|
||||
|
||||
### 我要部署到生產環境
|
||||
|
||||
1. [Docker 部署](deployment/docker.md)
|
||||
2. [反向代理](deployment/reverse-proxy.md)
|
||||
3. [安全性設定](configuration/security.md)
|
||||
4. [環境變數](configuration/environment-variables.md)
|
||||
1. [Docker 部署](部署指南/Docker.md)
|
||||
2. [反向代理](部署指南/反向代理.md)
|
||||
3. [安全性設定](配置設定/安全性.md)
|
||||
4. [環境變數](配置設定/環境變數.md)
|
||||
|
||||
### 我想參與開發
|
||||
|
||||
1. [專案結構](development/project-structure.md)
|
||||
2. [本地開發](development/local-development.md)
|
||||
3. [貢獻指南](development/contribution.md)
|
||||
4. [測試策略](testing/test-strategy.md)
|
||||
1. [專案結構](開發指南/專案結構.md)
|
||||
2. [本地開發](開發指南/本地開發.md)
|
||||
3. [貢獻指南](開發指南/貢獻指南.md)
|
||||
4. [測試策略](測試/測試策略.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🐳 [Docker 部署(含硬體加速)](deployment/docker.md)
|
||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
|
||||
> - 🐳 [Docker 部署(含硬體加速)](部署指南/Docker.md)
|
||||
> - 🔧 [反向代理設定](部署指南/反向代理.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - ⚙️ [環境變數設定](../configuration/environment-variables.md)
|
||||
> - 🔒 [安全性設定](../configuration/security.md)
|
||||
> - 🧹 [清理與限制](../configuration/limits-and-cleanup.md)
|
||||
> - ⚙️ [環境變數設定](../配置設定/環境變數.md)
|
||||
> - 🔒 [安全性設定](../配置設定/安全性.md)
|
||||
> - 🧹 [清理與限制](../配置設定/清理與限制.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🔒 [安全性設定](../configuration/security.md)
|
||||
> - 🔒 [安全性設定](../配置設定/安全性.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🔌 [轉換器完整說明](features/converters.md)
|
||||
> - 📊 [OCR 功能](features/ocr.md)
|
||||
> - 🌐 [翻譯功能](features/translation.md)
|
||||
> - 🔌 [轉換器完整說明](功能說明/轉換器.md)
|
||||
> - 📊 [OCR 功能](功能說明/OCR.md)
|
||||
> - 🌐 [翻譯功能](功能說明/翻譯.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🐳 [Docker 部署](deployment/docker.md)
|
||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
|
||||
> - 🔒 [安全性設定](configuration/security.md)
|
||||
> - 🐳 [Docker 部署](部署指南/Docker.md)
|
||||
> - 🔧 [反向代理設定](部署指南/反向代理.md)
|
||||
> - 🔒 [安全性設定](配置設定/安全性.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 📦 [Docker 部署指南](deployment/docker.md)
|
||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
|
||||
> - 📦 [Docker 部署指南](部署指南/Docker.md)
|
||||
> - 🔧 [反向代理設定](部署指南/反向代理.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - ❓ [常見問題 FAQ](getting-started/faq.md)
|
||||
> - ❓ [常見問題 FAQ](快速入門/常見問題.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -4,9 +4,9 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 📖 [概覽](getting-started/overview.md)
|
||||
> - 🚀 [快速開始](getting-started/quick-start.md)
|
||||
> - ❓ [常見問題](getting-started/faq.md)
|
||||
> - 📖 [概覽](快速入門/概覽.md)
|
||||
> - 🚀 [快速開始](快速入門/快速開始.md)
|
||||
> - ❓ [常見問題](快速入門/常見問題.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🌐 [多語言介面支援](features/i18n.md)
|
||||
> - 🌐 [多語言介面支援](功能說明/多語言.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
>
|
||||
> 本文件內容已整合至新的文件結構,請參閱:
|
||||
>
|
||||
> - 🛠️ [專案結構](development/project-structure.md)
|
||||
> - 🐳 [Docker 部署](deployment/docker.md)
|
||||
> - 🛠️ [專案結構](開發指南/專案結構.md)
|
||||
> - 🐳 [Docker 部署](部署指南/Docker.md)
|
||||
>
|
||||
> 此文件將在未來版本中移除。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# OCR 功能
|
||||
|
||||
ConvertX-CN 內建 Tesseract OCR,可將圖片中的文字轉換為可編輯文字。
|
||||
ConvertX-CN 內建 Tesseract OCR 與 ocrmypdf,提供完整的 OCR 功能:
|
||||
|
||||
- **圖片 OCR**:將圖片中的文字轉換為可編輯文字
|
||||
- **PDF OCR**:將掃描版 PDF 轉換為可搜尋 PDF(加入文字層)
|
||||
- **PDF 自動偵測**:翻譯引擎會自動偵測掃描版 PDF 並進行 OCR 處理
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -22,24 +26,51 @@ ConvertX-CN 完整版內建以下 OCR 語言:
|
|||
|
||||
## 使用方式
|
||||
|
||||
### PDF → 可搜尋 PDF(OCR)
|
||||
|
||||
將掃描版 PDF 轉換為可搜尋 PDF:
|
||||
|
||||
1. 上傳 PDF 檔案
|
||||
2. 選擇 Converter: `OCRmyPDF`
|
||||
3. 選擇目標語言:
|
||||
| 格式 | 說明 |
|
||||
| ---- | ---- |
|
||||
| `pdf-en` | English |
|
||||
| `pdf-zh-TW` | 繁體中文 |
|
||||
| `pdf-zh` | 簡體中文 |
|
||||
| `pdf-ja` | 日本語 |
|
||||
| `pdf-ko` | 한국어 |
|
||||
| `pdf-de` | Deutsch |
|
||||
| `pdf-fr` | Français |
|
||||
4. 進行轉換
|
||||
|
||||
> 💡 **功能特點**:
|
||||
>
|
||||
> - 自動偵測頁面方向並旋轉
|
||||
> - 自動校正傾斜
|
||||
> - 跳過已有文字層的頁面
|
||||
> - 詳細的處理進度輸出
|
||||
|
||||
### 圖片 → 文字
|
||||
|
||||
1. 上傳圖片(PNG, JPG, TIFF 等)
|
||||
2. 選擇目標格式 `txt` 或 `pdf`(可搜尋)
|
||||
3. 進行轉換
|
||||
|
||||
### PDF → 可搜尋 PDF
|
||||
### 掃描版 PDF 翻譯(自動處理)
|
||||
|
||||
1. 上傳掃描版 PDF
|
||||
2. 選擇 OCR 處理
|
||||
3. 獲得可搜尋的 PDF
|
||||
當使用 PDFMathTranslate 或 BabelDOC 翻譯 PDF 時:
|
||||
|
||||
1. 系統會**自動偵測**是否為掃描版 PDF(無文字層)
|
||||
2. 若為掃描版,系統會**自動執行 OCR** 加入文字層
|
||||
3. 翻譯引擎使用 OCR 處理後的 PDF 進行翻譯
|
||||
4. 使用者無需手動操作,全程自動完成
|
||||
|
||||
---
|
||||
|
||||
## 支援的輸入格式
|
||||
|
||||
- **點陣圖**:PNG, JPG, JPEG, TIFF, BMP, GIF
|
||||
- **文件**:PDF(掃描版)
|
||||
- **其他**:WebP, PNM, PBM
|
||||
|
||||
---
|
||||
|
|
@ -78,14 +109,18 @@ Tesseract 可同時辨識多種語言,但準確度可能下降。
|
|||
|
||||
### 方法一:自訂 Dockerfile
|
||||
|
||||
> 💡 在 `Dockerfile.full` 中取消註解以下內容
|
||||
|
||||
```dockerfile
|
||||
# 在 Dockerfile.full 中取消註解
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tesseract-ocr-spa \ # 西班牙文
|
||||
tesseract-ocr-ita \ # 義大利文
|
||||
tesseract-ocr-spa \
|
||||
tesseract-ocr-ita \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
```
|
||||
|
||||
- `tesseract-ocr-spa` — 西班牙文
|
||||
- `tesseract-ocr-ita` — 義大利文
|
||||
|
||||
### 方法二:掛載語言包
|
||||
|
||||
```yaml
|
||||
|
|
@ -112,6 +147,6 @@ volumes:
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [支援的轉換器](converters.md)
|
||||
- [翻譯功能](translation.md)
|
||||
- [Docker 部署](../deployment/docker.md)
|
||||
- [支援的轉換器](轉換器.md)
|
||||
- [翻譯功能](翻譯.md)
|
||||
- [Docker 部署](../部署指南/Docker.md)
|
||||
|
|
@ -128,5 +128,5 @@ http://localhost:3000/?lang=ja
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [貢獻指南](../development/contribution.md)
|
||||
- [專案結構](../development/project-structure.md)
|
||||
- [貢獻指南](../開發指南/貢獻指南.md)
|
||||
- [專案結構](../開發指南/專案結構.md)
|
||||
187
docs/功能說明/翻譯.md
Normal file
187
docs/功能說明/翻譯.md
Normal 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)
|
||||
|
|
@ -130,7 +130,7 @@ docker compose up -d
|
|||
- 繁體中文、簡體中文
|
||||
- 日文、韓文
|
||||
- 英文、德文、法文
|
||||
- 更多詳見 [多語言支援](../features/i18n.md)
|
||||
- 更多詳見 [多語言支援](../功能說明/多語言.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ docker compose up -d
|
|||
|
||||
### Q: 支援哪些格式?
|
||||
|
||||
1000+ 種格式,詳見 [支援的轉換器](../features/converters.md)
|
||||
1000+ 種格式,詳見 [支援的轉換器](../功能說明/轉換器.md)
|
||||
|
||||
### Q: 轉換失敗怎麼辦?
|
||||
|
||||
|
|
@ -169,11 +169,11 @@ client_max_body_size 500M;
|
|||
|
||||
### Q: 如何使用反向代理?
|
||||
|
||||
詳見 [反向代理設定](../deployment/reverse-proxy.md)
|
||||
詳見 [反向代理設定](../部署指南/反向代理.md)
|
||||
|
||||
### Q: 如何啟用硬體加速?
|
||||
|
||||
詳見 [進階配置 - 硬體加速](../deployment/docker.md#硬體加速)
|
||||
詳見 [進階配置 - 硬體加速](../部署指南/Docker.md#硬體加速)
|
||||
|
||||
### Q: 如何啟用 API Server?
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ client_max_body_size 500M;
|
|||
docker compose --profile api up -d
|
||||
```
|
||||
|
||||
詳見 [API 文件](../api/overview.md)
|
||||
詳見 [API 文件](../API/總覽.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ environment:
|
|||
|
||||
## 下一步
|
||||
|
||||
- 📖 [Docker 詳細配置](../deployment/docker.md)
|
||||
- ⚙️ [環境變數設定](../configuration/environment-variables.md)
|
||||
- 🔒 [安全性設定](../configuration/security.md)
|
||||
- 🔧 [反向代理設定](../deployment/reverse-proxy.md)
|
||||
- 📖 [Docker 詳細配置](../部署指南/Docker.md)
|
||||
- ⚙️ [環境變數設定](../配置設定/環境變數.md)
|
||||
- 🔒 [安全性設定](../配置設定/安全性.md)
|
||||
- 🔧 [反向代理設定](../部署指南/反向代理.md)
|
||||
|
|
@ -78,6 +78,6 @@ ConvertX-CN 是 fork 自 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
|
|||
|
||||
## 下一步
|
||||
|
||||
- 🚀 [快速開始](quick-start.md) — 5 分鐘內完成部署
|
||||
- ❓ [常見問題](faq.md) — 解決常見問題
|
||||
- 📦 [Docker 部署](../deployment/docker.md) — 詳細部署指南
|
||||
- 🚀 [快速開始](快速開始.md) — 5 分鐘內完成部署
|
||||
- ❓ [常見問題](常見問題.md) — 解決常見問題
|
||||
- 📦 [Docker 部署](../部署指南/Docker.md) — 詳細部署指南
|
||||
|
|
@ -143,6 +143,6 @@ docker build -t convertx-cn-test .
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [測試策略](test-strategy.md)
|
||||
- [E2E 測試](e2e-tests.md)
|
||||
- [貢獻指南](../development/contribution.md)
|
||||
- [測試策略](測試策略.md)
|
||||
- [E2E 測試](E2E測試.md)
|
||||
- [貢獻指南](../開發指南/貢獻指南.md)
|
||||
|
|
@ -176,6 +176,6 @@ npx playwright codegen http://localhost:3000
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [測試策略](test-strategy.md)
|
||||
- [CI/CD](ci-cd.md)
|
||||
- [本地開發](../development/local-development.md)
|
||||
- [測試策略](測試策略.md)
|
||||
- [CI/CD](CI-CD.md)
|
||||
- [本地開發](../開發指南/本地開發.md)
|
||||
|
|
@ -49,6 +49,6 @@ docker compose up -d
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [Docker 部署](../deployment/docker.md)
|
||||
- [環境變數](../configuration/environment-variables.md)
|
||||
- [反向代理](../deployment/reverse-proxy.md)
|
||||
- [Docker 部署](../部署指南/Docker.md)
|
||||
- [環境變數](../配置設定/環境變數.md)
|
||||
- [反向代理](../部署指南/反向代理.md)
|
||||
|
|
@ -97,21 +97,29 @@ docker run -d \
|
|||
|
||||
**重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
mkdir -p ~/convertx-cn/data
|
||||
**Linux / macOS:**
|
||||
|
||||
# Windows PowerShell
|
||||
```bash
|
||||
mkdir -p ~/convertx-cn/data
|
||||
```
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
mkdir C:\convertx-cn\data
|
||||
```
|
||||
|
||||
### 備份與還原
|
||||
|
||||
```bash
|
||||
# 備份
|
||||
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
|
||||
**備份:**
|
||||
|
||||
# 還原
|
||||
```bash
|
||||
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
|
||||
```
|
||||
|
||||
**還原:**
|
||||
|
||||
```bash
|
||||
tar -xzvf convertx-backup-20260120.tar.gz
|
||||
```
|
||||
|
||||
|
|
@ -199,18 +207,23 @@ services:
|
|||
|
||||
## 版本更新
|
||||
|
||||
```bash
|
||||
# 拉取最新版本
|
||||
docker pull convertx/convertx-cn:latest
|
||||
**1. 拉取最新版本:**
|
||||
|
||||
# 停止並移除舊容器
|
||||
```bash
|
||||
docker pull convertx/convertx-cn:latest
|
||||
```
|
||||
|
||||
**2. 停止並移除舊容器:**
|
||||
|
||||
```bash
|
||||
docker stop convertx-cn
|
||||
docker rm convertx-cn
|
||||
```
|
||||
|
||||
# 重新啟動(使用相同的參數)
|
||||
docker run -d \
|
||||
--name convertx-cn \
|
||||
# ... 其他參數
|
||||
**3. 重新啟動(使用相同的參數):**
|
||||
|
||||
```bash
|
||||
docker run -d --name convertx-cn ...
|
||||
```
|
||||
|
||||
或使用 Docker Compose:
|
||||
|
|
@ -228,7 +241,12 @@ docker compose up -d
|
|||
|
||||
```bash
|
||||
docker logs convertx-cn
|
||||
docker logs -f convertx-cn # 持續追蹤
|
||||
```
|
||||
|
||||
持續追蹤日誌:
|
||||
|
||||
```bash
|
||||
docker logs -f convertx-cn
|
||||
```
|
||||
|
||||
### 進入容器
|
||||
|
|
@ -251,5 +269,5 @@ docker exec -it convertx-cn /bin/bash
|
|||
## 相關文件
|
||||
|
||||
- [Docker Compose 詳解](docker-compose.md)
|
||||
- [反向代理設定](reverse-proxy.md)
|
||||
- [環境變數設定](../configuration/environment-variables.md)
|
||||
- [反向代理設定](反向代理.md)
|
||||
- [環境變數設定](../配置設定/環境變數.md)
|
||||
|
|
@ -111,7 +111,7 @@ environment:
|
|||
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||
```
|
||||
|
||||
完整環境變數說明請參考 [環境變數文件](../config/environment.md)。
|
||||
完整環境變數說明請參考 [環境變數文件](../配置設定/環境變數.md)。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -59,6 +59,8 @@ cd C:\convertx-cn
|
|||
|
||||
在專案資料夾建立 `docker-compose.yml`:
|
||||
|
||||
> 💡 `JWT_SECRET` 請改成你自己的隨機字串(至少 32 字元)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
convertx:
|
||||
|
|
@ -71,7 +73,7 @@ services:
|
|||
- ./data:/app/data
|
||||
environment:
|
||||
- TZ=Asia/Taipei
|
||||
- JWT_SECRET=請改成你自己的隨機字串至少32字元
|
||||
- JWT_SECRET=your-random-secret-at-least-32-chars
|
||||
```
|
||||
|
||||
### 必要參數說明
|
||||
|
|
@ -119,12 +121,12 @@ docker compose logs -f # Ctrl+C 退出
|
|||
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
||||
| 3000 埠被佔用 | 改用 `"8080:3000"` |
|
||||
|
||||
更多問題請參考 [FAQ](../faq.md)。
|
||||
更多問題請參考 [FAQ](../快速入門/常見問題.md)。
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [環境變數完整說明](../config/environment.md)
|
||||
- [反向代理與 HTTPS](../deployment.md)
|
||||
- [環境變數完整說明](../配置設定/環境變數.md)
|
||||
- [反向代理與 HTTPS](反向代理.md)
|
||||
- [版本更新方法](update.md)
|
||||
|
|
@ -24,12 +24,12 @@ docker compose up -d
|
|||
|
||||
### 更新到指定版本
|
||||
|
||||
修改 `docker-compose.yml`:
|
||||
修改 `docker-compose.yml`(指定要更新的版本號):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
convertx:
|
||||
image: convertx/convertx-cn:v0.1.9 # 指定版本
|
||||
image: convertx/convertx-cn:v0.1.9
|
||||
```
|
||||
|
||||
然後執行:
|
||||
|
|
@ -78,12 +78,12 @@ Copy-Item -Recurse .\data .\data.backup.$(Get-Date -Format "yyyyMMdd")
|
|||
|
||||
## 回滾版本
|
||||
|
||||
如果新版本有問題,可以回滾到舊版本:
|
||||
如果新版本有問題,可以回滾到舊版本(將版本號改為舊版本):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
convertx:
|
||||
image: convertx/convertx-cn:v0.1.8 # 舊版本
|
||||
image: convertx/convertx-cn:v0.1.8
|
||||
```
|
||||
|
||||
```bash
|
||||
|
|
@ -120,6 +120,8 @@ docker rmi convertx/convertx-cn:v0.1.7
|
|||
|
||||
可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器:
|
||||
|
||||
> 💡 `--interval 86400` — 每 24 小時檢查一次更新
|
||||
|
||||
```yaml
|
||||
services:
|
||||
convertx:
|
||||
|
|
@ -130,7 +132,7 @@ services:
|
|||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
command: --interval 86400 # 每 24 小時檢查一次
|
||||
command: --interval 86400
|
||||
```
|
||||
|
||||
> ⚠️ 自動更新適合測試環境。生產環境建議手動更新,確認新版本穩定後再升級。
|
||||
|
|
@ -8,10 +8,13 @@
|
|||
|
||||
透過反向代理存取時,請設定:
|
||||
|
||||
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
|
||||
- `HTTP_ALLOWED=false` — Proxy 已處理 HTTPS
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
|
||||
- HTTP_ALLOWED=false # Proxy 已處理 HTTPS
|
||||
- TRUST_PROXY=true
|
||||
- HTTP_ALLOWED=false
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -10,9 +10,11 @@
|
|||
|
||||
控制是否允許非 HTTPS 連線。
|
||||
|
||||
- `HTTP_ALLOWED=true` — 允許 HTTP(不安全,僅測試用)
|
||||
- `HTTP_ALLOWED=false` — 僅允許 HTTPS(預設)
|
||||
|
||||
```yaml
|
||||
- HTTP_ALLOWED=true # 允許 HTTP(不安全,僅測試用)
|
||||
- HTTP_ALLOWED=false # 僅允許 HTTPS(預設)
|
||||
- HTTP_ALLOWED=false
|
||||
```
|
||||
|
||||
#### 運作原理
|
||||
|
|
@ -40,9 +42,11 @@
|
|||
|
||||
是否信任反向代理傳來的 headers。
|
||||
|
||||
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
|
||||
- `TRUST_PROXY=false` — 不信任(預設)
|
||||
|
||||
```yaml
|
||||
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
|
||||
- TRUST_PROXY=false # 不信任(預設)
|
||||
- TRUST_PROXY=false
|
||||
```
|
||||
|
||||
#### 運作原理
|
||||
|
|
@ -69,9 +73,11 @@
|
|||
|
||||
### ACCOUNT_REGISTRATION
|
||||
|
||||
- `ACCOUNT_REGISTRATION=true` — 開放註冊
|
||||
- `ACCOUNT_REGISTRATION=false` — 關閉註冊
|
||||
|
||||
```yaml
|
||||
- ACCOUNT_REGISTRATION=true # 開放註冊
|
||||
- ACCOUNT_REGISTRATION=false # 關閉註冊
|
||||
- ACCOUNT_REGISTRATION=false
|
||||
```
|
||||
|
||||
#### 建議流程
|
||||
|
|
@ -95,22 +101,41 @@
|
|||
|
||||
#### 產生方式
|
||||
|
||||
```bash
|
||||
# Linux / macOS
|
||||
openssl rand -hex 32
|
||||
**Linux / macOS:**
|
||||
|
||||
# 輸出範例:a1b2c3d4e5f6789...(64 字元)
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
|
||||
```
|
||||
|
||||
**線上工具:**
|
||||
|
||||
- https://generate-secret.vercel.app/32
|
||||
|
||||
**Node.js:**
|
||||
|
||||
```bash
|
||||
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
> 💡 產生的密鑰應為 32-64 字元的隨機字串,例如:`a1b2c3d4e5f6789...`
|
||||
|
||||
---
|
||||
|
||||
## 公開服務安全
|
||||
|
||||
### ALLOW_UNAUTHENTICATED
|
||||
|
||||
- `ALLOW_UNAUTHENTICATED=true` — 允許未登入使用
|
||||
- `ALLOW_UNAUTHENTICATED=false` — 必須登入(預設)
|
||||
|
||||
```yaml
|
||||
- ALLOW_UNAUTHENTICATED=true # 允許未登入使用
|
||||
- ALLOW_UNAUTHENTICATED=false # 必須登入(預設)
|
||||
- ALLOW_UNAUTHENTICATED=false
|
||||
```
|
||||
|
||||
#### 風險
|
||||
|
|
@ -125,12 +150,16 @@ openssl rand -hex 32
|
|||
|
||||
若需要公開服務,建議:
|
||||
|
||||
- `AUTO_DELETE_EVERY_N_HOURS=1` — 頻繁清理
|
||||
- `HIDE_HISTORY=true` — 隱藏歷史
|
||||
- `MAX_CONVERT_PROCESS=2` — 限制同時轉換數
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- ALLOW_UNAUTHENTICATED=true
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1 # 頻繁清理
|
||||
- HIDE_HISTORY=true # 隱藏歷史
|
||||
- MAX_CONVERT_PROCESS=2 # 限制同時轉換數
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1
|
||||
- HIDE_HISTORY=true
|
||||
- MAX_CONVERT_PROCESS=2
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -139,31 +168,36 @@ environment:
|
|||
|
||||
### 防火牆
|
||||
|
||||
只開放必要的埠:
|
||||
只開放必要的埠。
|
||||
|
||||
**UFW (Ubuntu) 開放單一埠:**
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu)
|
||||
sudo ufw allow 3000/tcp
|
||||
```
|
||||
|
||||
# 或只允許特定 IP
|
||||
**只允許特定 IP 範圍:**
|
||||
|
||||
```bash
|
||||
sudo ufw allow from 192.168.1.0/24 to any port 3000
|
||||
```
|
||||
|
||||
### 只允許本機存取
|
||||
|
||||
> 💡 `127.0.0.1:3000:3000` 只有本機可存取
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000" # 只有本機可存取
|
||||
- "127.0.0.1:3000:3000"
|
||||
```
|
||||
|
||||
搭配反向代理提供對外服務。
|
||||
|
||||
### 限制上傳大小
|
||||
|
||||
在反向代理層限制:
|
||||
在反向代理層限制(Nginx 範例):
|
||||
|
||||
```nginx
|
||||
# Nginx
|
||||
client_max_body_size 100M;
|
||||
```
|
||||
|
||||
|
|
@ -173,21 +207,25 @@ client_max_body_size 100M;
|
|||
|
||||
### 定期清理
|
||||
|
||||
> 💡 每 24 小時自動清理過期檔案
|
||||
|
||||
```yaml
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24 # 每 24 小時清理
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||
```
|
||||
|
||||
### 備份
|
||||
|
||||
使用 crontab 設定定期備份(每天凌晨 2 點):
|
||||
|
||||
```bash
|
||||
# 定期備份資料
|
||||
0 2 * * * tar -czvf /backup/convertx-$(date +\%Y\%m\%d).tar.gz /path/to/data
|
||||
```
|
||||
|
||||
### 權限設定
|
||||
|
||||
限制資料夾權限,僅允許擁有者存取:
|
||||
|
||||
```bash
|
||||
# 限制資料夾權限
|
||||
chmod 700 ./data
|
||||
```
|
||||
|
||||
|
|
@ -214,6 +252,6 @@ chmod 700 ./data
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [環境變數設定](environment-variables.md)
|
||||
- [反向代理設定](../deployment/reverse-proxy.md)
|
||||
- [Docker 部署](../deployment/docker.md)
|
||||
- [環境變數設定](環境變數.md)
|
||||
- [反向代理設定](../部署指南/反向代理.md)
|
||||
- [Docker 部署](../部署指南/Docker.md)
|
||||
|
|
@ -16,16 +16,13 @@
|
|||
| 停用 | `0` |
|
||||
| 單位 | 小時 |
|
||||
|
||||
- `AUTO_DELETE_EVERY_N_HOURS=24` — 每 24 小時清理一次
|
||||
- `AUTO_DELETE_EVERY_N_HOURS=1` — 每 1 小時清理一次(公開服務建議)
|
||||
- `AUTO_DELETE_EVERY_N_HOURS=0` — 停用自動清理
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# 每 24 小時清理一次
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||
|
||||
# 每 1 小時清理一次(公開服務建議)
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1
|
||||
|
||||
# 停用自動清理
|
||||
- AUTO_DELETE_EVERY_N_HOURS=0
|
||||
```
|
||||
|
||||
### 清理範圍
|
||||
|
|
@ -49,13 +46,12 @@ environment:
|
|||
| ------ | ------------- |
|
||||
| 預設值 | `0`(無限制) |
|
||||
|
||||
- `MAX_CONVERT_PROCESS=2` — 最多同時 2 個轉換任務
|
||||
- `MAX_CONVERT_PROCESS=0` — 無限制
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# 最多同時 2 個轉換任務
|
||||
- MAX_CONVERT_PROCESS=2
|
||||
|
||||
# 無限制
|
||||
- MAX_CONVERT_PROCESS=0
|
||||
```
|
||||
|
||||
#### 使用建議
|
||||
|
|
@ -152,17 +148,22 @@ find ./data/output -mtime +7 -delete
|
|||
|
||||
### 個人使用
|
||||
|
||||
- `AUTO_DELETE_EVERY_N_HOURS=168` — 一週清理一次
|
||||
- `MAX_CONVERT_PROCESS=0` — 無限制
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- AUTO_DELETE_EVERY_N_HOURS=168 # 一週
|
||||
- MAX_CONVERT_PROCESS=0 # 無限制
|
||||
- AUTO_DELETE_EVERY_N_HOURS=168
|
||||
- MAX_CONVERT_PROCESS=0
|
||||
```
|
||||
|
||||
### 小型團隊
|
||||
|
||||
> 💡 `AUTO_DELETE_EVERY_N_HOURS=24` — 每天清理一次
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24 # 一天
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||
- MAX_CONVERT_PROCESS=4
|
||||
|
||||
deploy:
|
||||
|
|
@ -173,9 +174,11 @@ deploy:
|
|||
|
||||
### 公開服務
|
||||
|
||||
> 💡 `AUTO_DELETE_EVERY_N_HOURS=1` — 每小時清理一次
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1 # 一小時
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1
|
||||
- MAX_CONVERT_PROCESS=2
|
||||
- ALLOW_UNAUTHENTICATED=true
|
||||
- HIDE_HISTORY=true
|
||||
|
|
@ -191,6 +194,6 @@ deploy:
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [環境變數設定](environment-variables.md)
|
||||
- [安全性設定](security.md)
|
||||
- [Docker 部署](../deployment/docker.md)
|
||||
- [環境變數設定](環境變數.md)
|
||||
- [安全性設定](安全性.md)
|
||||
- [Docker 部署](../部署指南/Docker.md)
|
||||
|
|
@ -174,6 +174,6 @@ api-server/
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [本地開發](local-development.md)
|
||||
- [貢獻指南](contribution.md)
|
||||
- [測試策略](../testing/test-strategy.md)
|
||||
- [本地開發](本地開發.md)
|
||||
- [貢獻指南](貢獻指南.md)
|
||||
- [測試策略](../測試/測試策略.md)
|
||||
|
|
@ -187,6 +187,6 @@ docs: update deployment guide
|
|||
|
||||
## 相關文件
|
||||
|
||||
- [專案結構](project-structure.md)
|
||||
- [本地開發](local-development.md)
|
||||
- [測試策略](../testing/test-strategy.md)
|
||||
- [專案結構](專案結構.md)
|
||||
- [本地開發](本地開發.md)
|
||||
- [測試策略](../測試/測試策略.md)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "convertx-frontend",
|
||||
"version": "0.1.13",
|
||||
"version": "0.1.14",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.tsx",
|
||||
"hot": "bun run --hot src/index.tsx",
|
||||
|
|
@ -60,5 +60,6 @@
|
|||
"@parcel/watcher",
|
||||
"@tailwindcss/oxide",
|
||||
"oxc-resolver"
|
||||
]
|
||||
],
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { join, basename, dirname } from "node:path";
|
||||
import { ExecFileFn } from "./types";
|
||||
import { getArchiveFileName } from "../transfer";
|
||||
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
|
||||
|
||||
/**
|
||||
* BabelDOC Content Engine
|
||||
|
|
@ -275,7 +276,19 @@ export async function convert(
|
|||
_options?: unknown,
|
||||
execFile: ExecFileFn = execFileOriginal,
|
||||
): Promise<string> {
|
||||
let ocrTempFile: string | undefined;
|
||||
|
||||
try {
|
||||
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
|
||||
console.log(`[BabelDOC] Checking if PDF needs OCR...`);
|
||||
const ocrResult = await ensureSearchablePdf(filePath, execFile);
|
||||
const inputPdf = ocrResult.path;
|
||||
ocrTempFile = ocrResult.tempFile;
|
||||
|
||||
if (ocrResult.wasOcred) {
|
||||
console.log(`[BabelDOC] ✅ Scanned PDF detected and OCR'd automatically`);
|
||||
}
|
||||
|
||||
// 1. 檢查資源(警告但不阻止)
|
||||
checkResourcesExist();
|
||||
|
||||
|
|
@ -300,8 +313,8 @@ export async function convert(
|
|||
// 5. 設定 BabelDOC 輸出路徑(依輸出格式決定副檔名)
|
||||
const translatedFilePath = join(tempDir, `${inputFileName}-translated.${outputExt}`);
|
||||
|
||||
// 6. 執行 babeldoc 翻譯
|
||||
await runBabelDoc(filePath, translatedFilePath, targetLang, outputFormat, execFile);
|
||||
// 6. 執行 babeldoc 翻譯(使用 OCR 處理後的 PDF)
|
||||
await runBabelDoc(inputPdf, translatedFilePath, targetLang, outputFormat, execFile);
|
||||
|
||||
// 7. 複製翻譯後的檔案到封裝目錄
|
||||
const translatedDest = join(archiveDir, `translated-${targetLang}.${outputExt}`);
|
||||
|
|
@ -345,8 +358,13 @@ export async function convert(
|
|||
// 10. 清理臨時目錄
|
||||
removeDir(tempDir);
|
||||
|
||||
// 11. 清理 OCR 暫存檔案
|
||||
cleanupOcrTempFile(ocrTempFile);
|
||||
|
||||
return "Done";
|
||||
} catch (error) {
|
||||
// 確保清理 OCR 暫存檔案
|
||||
cleanupOcrTempFile(ocrTempFile);
|
||||
throw new Error(`BabelDOC error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
properties as propertiesPDFMathTranslate,
|
||||
} from "./pdfmathtranslate";
|
||||
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
|
||||
import { convert as convertOcrMyPdf, properties as propertiesOcrMyPdf } from "./ocrmypdf";
|
||||
|
||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||
|
||||
|
|
@ -156,6 +157,10 @@ const properties: Record<
|
|||
properties: propertiesBabelDoc,
|
||||
converter: convertBabelDoc,
|
||||
},
|
||||
OCRmyPDF: {
|
||||
properties: propertiesOcrMyPdf,
|
||||
converter: convertOcrMyPdf,
|
||||
},
|
||||
};
|
||||
|
||||
function chunks<T>(arr: T[], size: number): T[][] {
|
||||
|
|
|
|||
271
src/converters/ocrmypdf.ts
Normal file
271
src/converters/ocrmypdf.ts
Normal 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/5:PDF 已有文字層,跳過 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}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { execFile as execFileOriginal } from "node:child_process";
|
|||
import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, copyFileSync } from "node:fs";
|
||||
import { join, basename, dirname } from "node:path";
|
||||
import { getArchiveFileName } from "../transfer";
|
||||
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
|
||||
import type { ExecFileFn } from "./types";
|
||||
|
||||
// 翻譯服務優先順序(自動 fallback)
|
||||
|
|
@ -339,7 +340,19 @@ export async function convert(
|
|||
_options?: unknown,
|
||||
execFile: ExecFileFn = execFileOriginal,
|
||||
): Promise<string> {
|
||||
let ocrTempFile: string | undefined;
|
||||
|
||||
try {
|
||||
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
|
||||
console.log(`[PDFMathTranslate] Checking if PDF needs OCR...`);
|
||||
const ocrResult = await ensureSearchablePdf(filePath, execFile);
|
||||
const inputPdf = ocrResult.path;
|
||||
ocrTempFile = ocrResult.tempFile;
|
||||
|
||||
if (ocrResult.wasOcred) {
|
||||
console.log(`[PDFMathTranslate] ✅ Scanned PDF detected and OCR'd automatically`);
|
||||
}
|
||||
|
||||
// 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
|
||||
checkModelsExist();
|
||||
|
||||
|
|
@ -361,8 +374,8 @@ export async function convert(
|
|||
const archiveDir = join(tempDir, "archive");
|
||||
mkdirSync(archiveDir, { recursive: true });
|
||||
|
||||
// 5. 執行 pdf2zh 翻譯
|
||||
const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile);
|
||||
// 5. 執行 pdf2zh 翻譯(使用 OCR 處理後的 PDF)
|
||||
const { monoPath, dualPath } = await runPdf2zh(inputPdf, tempDir, targetLang, execFile);
|
||||
|
||||
// 6. 複製翻譯後的檔案到封裝目錄
|
||||
// PDFMathTranslate 輸出:
|
||||
|
|
@ -440,8 +453,13 @@ export async function convert(
|
|||
// 9. 清理臨時目錄
|
||||
removeDir(tempDir);
|
||||
|
||||
// 10. 清理 OCR 暫存檔案
|
||||
cleanupOcrTempFile(ocrTempFile);
|
||||
|
||||
return "Done";
|
||||
} catch (error) {
|
||||
// 確保清理 OCR 暫存檔案
|
||||
cleanupOcrTempFile(ocrTempFile);
|
||||
throw new Error(`PDFMathTranslate error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
202
src/helpers/pdfOcr.ts
Normal file
202
src/helpers/pdfOcr.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,7 +67,10 @@ describe("BabelDOC converter - Chinese translation", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
// 返回超過 100 個字元,表示 PDF 不需要 OCR
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
babeldocCalled = true;
|
||||
babeldocArgs = args;
|
||||
|
||||
|
|
@ -131,7 +134,9 @@ describe("BabelDOC converter - English translation", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
babeldocArgs = args;
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
|
|
@ -181,7 +186,9 @@ describe("BabelDOC converter - Traditional Chinese", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
babeldocArgs = args;
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
|
|
@ -231,7 +238,9 @@ describe("BabelDOC converter - Output structure", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
const outputPath = args[outputIndex + 1];
|
||||
|
|
@ -267,7 +276,9 @@ describe("BabelDOC converter - Output structure", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
const outputPath = args[outputIndex + 1];
|
||||
|
|
@ -317,7 +328,9 @@ describe("BabelDOC converter - Error handling", () => {
|
|||
_args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
const error = new Error("Translation failed") as ExecFileException;
|
||||
error.code = 1;
|
||||
callback(error, "", "babeldoc: Translation error");
|
||||
|
|
@ -337,7 +350,9 @@ describe("BabelDOC converter - Error handling", () => {
|
|||
_args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
// Don't create output file, simulating failed translation
|
||||
callback(null, "Complete", "");
|
||||
}
|
||||
|
|
@ -376,7 +391,9 @@ describe("BabelDOC converter - Markdown output format", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
babeldocArgs = args;
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
|
|
@ -428,7 +445,9 @@ describe("BabelDOC converter - HTML output format", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "babeldoc") {
|
||||
if (cmd === "pdftotext") {
|
||||
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||
} else if (cmd === "babeldoc") {
|
||||
babeldocArgs = args;
|
||||
const outputIndex = args.indexOf("-o");
|
||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe("PDFMathTranslate converter - Chinese translation", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
pdf2zhCalled = true;
|
||||
pdf2zhArgs = args;
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ describe("PDFMathTranslate converter - English translation", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
pdf2zhArgs = args;
|
||||
const outputDirIndex = args.indexOf("-o");
|
||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||
|
|
@ -165,7 +165,7 @@ describe("PDFMathTranslate converter - Japanese translation", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
pdf2zhArgs = args;
|
||||
const outputDirIndex = args.indexOf("-o");
|
||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||
|
|
@ -214,7 +214,7 @@ describe("PDFMathTranslate converter - Traditional Chinese", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
pdf2zhArgs = args;
|
||||
const outputDirIndex = args.indexOf("-o");
|
||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||
|
|
@ -264,7 +264,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
const outputDirIndex = args.indexOf("-o");
|
||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||
const outputDir = args[outputDirIndex + 1];
|
||||
|
|
@ -308,7 +308,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
|
|||
args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
const outputDirIndex = args.indexOf("-o");
|
||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||
const outputDir = args[outputDirIndex + 1];
|
||||
|
|
@ -362,7 +362,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
|
|||
_args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
const error = new Error("Translation failed") as ExecFileException;
|
||||
callback(error, "", "Error: Translation service unavailable");
|
||||
}
|
||||
|
|
@ -381,7 +381,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
|
|||
_args: string[],
|
||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||
) => {
|
||||
if (cmd === "pdf2zh") {
|
||||
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||
// Don't create any output files
|
||||
callback(null, "Complete", "");
|
||||
} else if (cmd === "tar") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue