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
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.14](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.14) (2026-01-23)
|
||||||
|
|
||||||
|
OCR 功能強化版本,新增 OCRmyPDF 轉換引擎。
|
||||||
|
|
||||||
|
### ✨ Features
|
||||||
|
|
||||||
|
- **OCRmyPDF 轉換引擎**:新增獨立的 OCR 轉換引擎,支援將掃描版 PDF 轉換為可搜尋 PDF
|
||||||
|
- 支援 7 種語言:英文、繁體中文、簡體中文、日文、韓文、德文、法文
|
||||||
|
- 與 PDFMathTranslate 風格一致的 UI 格式(`pdf-en`、`pdf-zh-TW` 等)
|
||||||
|
- 自動偵測頁面方向並旋轉
|
||||||
|
- 自動校正傾斜
|
||||||
|
- 跳過已有文字層的頁面
|
||||||
|
- 詳細的5階段處理進度輸出
|
||||||
|
|
||||||
|
### 📦 Build
|
||||||
|
|
||||||
|
- **Dockerfile**:安裝 ocrmypdf 與 Tesseract OCR 語言包(階段 9/11)
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- 更新 OCR 功能文件,說明 OCRmyPDF 轉換引擎用法
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.13](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.13) (2026-01-23)
|
## [0.1.13](https://github.com/pi-docket/ConvertX-CN/releases/tag/v0.1.13) (2026-01-23)
|
||||||
|
|
||||||
工作流程優化與測試修復版本。
|
工作流程優化與測試修復版本。
|
||||||
|
|
|
||||||
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
|
&& echo 'DPkg::Lock::Timeout "120";' >> /etc/apt/apt.conf.d/80-retries
|
||||||
|
|
||||||
# 階段 1:基礎系統工具
|
# 階段 1:基礎系統工具
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 1/11:安裝基礎系統工具" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
locales \
|
locales \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 1/11 完成:基礎系統工具已安裝"
|
||||||
|
|
||||||
# 階段 2:核心轉換工具(小型)
|
# 階段 2:核心轉換工具(小型)
|
||||||
# 注意:dasel 和 resvg 在 bookworm 中不存在,後續用二進位檔案安裝
|
# 注意:dasel 和 resvg 在 bookworm 中不存在,後續用二進位檔案安裝
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 2/11:安裝核心轉換工具" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
assimp-utils \
|
assimp-utils \
|
||||||
dcraw \
|
dcraw \
|
||||||
dvisvgm \
|
dvisvgm \
|
||||||
|
|
@ -133,64 +142,95 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
mupdf-tools \
|
mupdf-tools \
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
potrace \
|
potrace \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 2/11 完成:核心轉換工具已安裝"
|
||||||
|
|
||||||
# 階段 2.1:安裝 dasel(從 GitHub 下載二進位檔案)
|
# 階段 2.1:安裝 dasel(從 GitHub 下載二進位檔案)
|
||||||
RUN ARCH=$(uname -m) && \
|
RUN echo "" && \
|
||||||
|
echo " 🔧 階段 2.1:安裝 dasel..." && \
|
||||||
|
ARCH=$(uname -m) && \
|
||||||
if [ "$ARCH" = "aarch64" ]; then \
|
if [ "$ARCH" = "aarch64" ]; then \
|
||||||
DASEL_ARCH="linux_arm64"; \
|
DASEL_ARCH="linux_arm64"; \
|
||||||
else \
|
else \
|
||||||
DASEL_ARCH="linux_amd64"; \
|
DASEL_ARCH="linux_amd64"; \
|
||||||
fi && \
|
fi && \
|
||||||
curl -sSLf "https://github.com/TomWright/dasel/releases/download/v2.8.1/dasel_${DASEL_ARCH}" -o /usr/local/bin/dasel && \
|
curl -sSLf "https://github.com/TomWright/dasel/releases/download/v2.8.1/dasel_${DASEL_ARCH}" -o /usr/local/bin/dasel && \
|
||||||
chmod +x /usr/local/bin/dasel
|
chmod +x /usr/local/bin/dasel && \
|
||||||
|
echo " ✅ dasel 安裝完成"
|
||||||
|
|
||||||
# 階段 2.2:安裝 resvg(從 GitHub 下載二進位檔案)
|
# 階段 2.2:安裝 resvg(從 GitHub 下載二進位檔案)
|
||||||
# 注意:resvg 官方只提供 x86_64 版本,ARM64 需從源碼編譯或跳過
|
# 注意:resvg 官方只提供 x86_64 版本,ARM64 需從源碼編譯或跳過
|
||||||
RUN ARCH=$(uname -m) && \
|
RUN echo "" && \
|
||||||
|
echo " 🔧 階段 2.2:安裝 resvg..." && \
|
||||||
|
ARCH=$(uname -m) && \
|
||||||
if [ "$ARCH" = "aarch64" ]; then \
|
if [ "$ARCH" = "aarch64" ]; then \
|
||||||
echo "⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝(可改用 ImageMagick 或 Inkscape 替代)"; \
|
echo " ⚠️ resvg 沒有 ARM64 預編譯版本,跳過安裝"; \
|
||||||
else \
|
else \
|
||||||
curl -sSLf "https://github.com/linebender/resvg/releases/download/v0.44.0/resvg-linux-x86_64.tar.gz" -o /tmp/resvg.tar.gz && \
|
curl -sSLf "https://github.com/linebender/resvg/releases/download/v0.44.0/resvg-linux-x86_64.tar.gz" -o /tmp/resvg.tar.gz && \
|
||||||
tar -xzf /tmp/resvg.tar.gz -C /tmp/ && \
|
tar -xzf /tmp/resvg.tar.gz -C /tmp/ && \
|
||||||
mv /tmp/resvg /usr/local/bin/resvg && \
|
mv /tmp/resvg /usr/local/bin/resvg && \
|
||||||
chmod +x /usr/local/bin/resvg && \
|
chmod +x /usr/local/bin/resvg && \
|
||||||
rm -rf /tmp/resvg.tar.gz; \
|
rm -rf /tmp/resvg.tar.gz && \
|
||||||
|
echo " ✅ resvg 安裝完成"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 階段 3:影音處理工具
|
# 階段 3:影音處理工具
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 3/11:安裝影音處理工具" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libavcodec-extra \
|
libavcodec-extra \
|
||||||
libva2 \
|
libva2 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 3/11 完成:影音處理工具已安裝(ffmpeg)"
|
||||||
|
|
||||||
# 階段 4:圖像處理工具
|
# 階段 4:圖像處理工具
|
||||||
# 注意:bookworm 使用 imagemagick(版本 6),trixie 才有 imagemagick-7
|
# 注意:bookworm 使用 imagemagick(版本 6),trixie 才有 imagemagick-7
|
||||||
# 注意:Inkscape 需要 xvfb 在無 DISPLAY 環境下執行某些操作(如 PNG 轉 SVG)
|
# 注意:Inkscape 需要 xvfb 在無 DISPLAY 環境下執行某些操作(如 PNG 轉 SVG)
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 4/11:安裝圖像處理工具" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
imagemagick \
|
imagemagick \
|
||||||
inkscape \
|
inkscape \
|
||||||
libheif-examples \
|
libheif-examples \
|
||||||
libjxl-tools \
|
libjxl-tools \
|
||||||
libvips-tools \
|
libvips-tools \
|
||||||
xvfb \
|
xvfb \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 4/11 完成:圖像處理工具已安裝(ImageMagick, Inkscape, VIPS)"
|
||||||
|
|
||||||
# 階段 5:文件處理工具
|
# 階段 5:文件處理工具
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 5/11:安裝文件處理工具" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
calibre \
|
calibre \
|
||||||
libemail-outlook-message-perl \
|
libemail-outlook-message-perl \
|
||||||
pandoc \
|
pandoc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 5/11 完成:文件處理工具已安裝(Calibre, Pandoc)"
|
||||||
|
|
||||||
# 階段 6:LibreOffice(最大的套件,單獨安裝)
|
# 階段 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 \
|
libreoffice \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 6/11 完成:LibreOffice 已安裝"
|
||||||
|
|
||||||
# 階段 7:TexLive 基礎
|
# 階段 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-base \
|
||||||
texlive-latex-base \
|
texlive-latex-base \
|
||||||
texlive-latex-recommended \
|
texlive-latex-recommended \
|
||||||
|
|
@ -198,19 +238,29 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
texlive-xetex \
|
texlive-xetex \
|
||||||
latexmk \
|
latexmk \
|
||||||
lmodern \
|
lmodern \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 7/11 完成:TexLive 基礎已安裝"
|
||||||
|
|
||||||
# 階段 8: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-cjk \
|
||||||
texlive-lang-german \
|
texlive-lang-german \
|
||||||
texlive-lang-french \
|
texlive-lang-french \
|
||||||
texlive-lang-arabic \
|
texlive-lang-arabic \
|
||||||
texlive-lang-other \
|
texlive-lang-other \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 8/11 完成:TexLive 語言包已安裝"
|
||||||
|
|
||||||
# 階段 9:OCR 支援
|
# 階段 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 \
|
||||||
tesseract-ocr-eng \
|
tesseract-ocr-eng \
|
||||||
tesseract-ocr-chi-tra \
|
tesseract-ocr-chi-tra \
|
||||||
|
|
@ -219,25 +269,41 @@ RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
tesseract-ocr-kor \
|
tesseract-ocr-kor \
|
||||||
tesseract-ocr-deu \
|
tesseract-ocr-deu \
|
||||||
tesseract-ocr-fra \
|
tesseract-ocr-fra \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
ocrmypdf \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 9/11 完成:OCR 支援已安裝(7 種語言)"
|
||||||
|
|
||||||
# 階段 10:字型
|
# 階段 10:字型
|
||||||
RUN apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
RUN echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "📦 階段 10/11:安裝字型(Noto CJK + Liberation)" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
apt-get update --fix-missing && apt-get install -y --no-install-recommends \
|
||||||
fonts-noto-cjk \
|
fonts-noto-cjk \
|
||||||
fonts-noto-core \
|
fonts-noto-core \
|
||||||
fonts-noto-color-emoji \
|
fonts-noto-color-emoji \
|
||||||
fonts-liberation \
|
fonts-liberation \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 10/11 完成:字型已安裝"
|
||||||
|
|
||||||
# 階段 11:Python 依賴
|
# 階段 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 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-numpy \
|
python3-numpy \
|
||||||
python3-tinycss2 \
|
python3-tinycss2 \
|
||||||
python3-opencv \
|
python3-opencv \
|
||||||
pipx \
|
pipx \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* && \
|
||||||
|
echo "✅ 階段 11/11 完成:Python 依賴已安裝" && \
|
||||||
|
echo "" && \
|
||||||
|
echo "========================================" && \
|
||||||
|
echo "✅ 所有 APT 套件安裝完成!" && \
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 🔥 階段 12-UNIFIED:Python 工具安裝 + 模型下載(單一 RUN 原則)
|
# 🔥 階段 12-UNIFIED:Python 工具安裝 + 模型下載(單一 RUN 原則)
|
||||||
|
|
|
||||||
160
LICENSE
160
LICENSE
|
|
@ -13,7 +13,7 @@ cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users.
|
software for all its users.
|
||||||
|
|
||||||
|
|
@ -24,10 +24,10 @@ them if you wish), that you receive source code or can get it if you
|
||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
you this License which gives you legal permission to copy, distribute
|
which gives you legal permission to copy, distribute and/or modify the
|
||||||
and/or modify the software.
|
software.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
A secondary benefit of defending all users' freedom is that
|
||||||
improvements made in alternate versions of the program, if they
|
improvements made in alternate versions of the program, if they
|
||||||
|
|
@ -48,10 +48,10 @@ a publicly accessible server, gives the public access to the source
|
||||||
code of the modified version.
|
code of the modified version.
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
An older license, called the Affero General Public License and
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
published by Affero, was designed to accomplish similar goals. This
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
is a different license, not a version of the Affero GPL, but Affero
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
has released a new version of the Affero GPL which permits relicensing
|
||||||
this license.
|
under this license.
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
|
|
@ -60,19 +60,20 @@ modification follow.
|
||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public
|
||||||
|
License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds
|
||||||
works, such as semiconductor masks.
|
of works, such as semiconductor masks.
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
"recipients" may be individuals or organizations.
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
To "modify" a work means to copy from or adapt all or part of the
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
work in a fashion requiring copyright permission, other than the making
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
of an exact copy. The resulting work is called a "modified version"
|
||||||
earlier work or a work "based on" the earlier work.
|
of the earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
A "covered work" means either the unmodified Program or a work based
|
||||||
on the Program.
|
on the Program.
|
||||||
|
|
@ -317,8 +318,8 @@ been installed in ROM).
|
||||||
The requirement to provide Installation Information does not include a
|
The requirement to provide Installation Information does not include a
|
||||||
requirement to continue to provide support service, warranty, or updates
|
requirement to continue to provide support service, warranty, or updates
|
||||||
for a work that has been modified or installed by the recipient, or for
|
for a work that has been modified or installed by the recipient, or for
|
||||||
the User Product in which it has been modified or installed. Access to a
|
the User Product in which it has been modified or installed. Access to
|
||||||
network may be denied when the modification itself materially and
|
a network may be denied when the modification itself materially and
|
||||||
adversely affects the operation of the network or violates the rules and
|
adversely affects the operation of the network or violates the rules and
|
||||||
protocols for communication across the network.
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
|
@ -347,8 +348,8 @@ additional permissions on material, added by you to a covered work,
|
||||||
for which you have or can give appropriate copyright permission.
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
Notwithstanding any other provision of this License, for material you
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
add to a covered work, you may (if authorized by the copyright holders
|
||||||
that material) supplement the terms of this License with terms:
|
of that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
terms of sections 15 and 16 of this License; or
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
@ -531,53 +532,44 @@ otherwise be available to you under applicable patent law.
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
License and any other pertinent obligations, then as a consequence you
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
may not convey it at all. For example, if you agree to terms that
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
obligate you to collect a royalty for further conveying from those to
|
||||||
the Program, the only way you could satisfy both those terms and this
|
whom you convey the Program, the only way you could satisfy both those
|
||||||
License would be to refrain entirely from conveying the Program.
|
terms and this License would be to refrain entirely from conveying the
|
||||||
|
Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU General Public License.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
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
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the work with which it is combined will remain governed by version
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
of the GNU Affero General Public License from time to time. Such new
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
versions will be similar in spirit to the present version, but may
|
||||||
address new problems or concerns.
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Program specifies that a certain numbered version of the GNU Affero
|
||||||
Public License "or any later version" applies to it, you have the
|
General Public License "or any later version" applies to it, you have
|
||||||
option of following the terms and conditions either of that numbered
|
the option of following the terms and conditions either of that
|
||||||
version or of any later version published by the Free Software
|
numbered version or of any later version published by the Free
|
||||||
Foundation. If the Program does not specify a version number of the
|
Software Foundation. If the Program does not specify a version number
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
of the GNU Affero General Public License, you may choose any version
|
||||||
by the Free Software Foundation.
|
ever published by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that
|
||||||
public statement of acceptance of a version permanently authorizes you
|
proxy's public statement of acceptance of a version permanently
|
||||||
to choose that version for the Program.
|
authorizes you to choose that version for the Program.
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
Later license versions may give you additional or different
|
||||||
permissions. However, no additional obligations are imposed on any
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
|
@ -591,21 +583,21 @@ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
16. Limitation of Liability.
|
16. Limitation of Liability.
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
|
||||||
SUCH DAMAGES.
|
THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
|
@ -617,45 +609,3 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||||
copy of the Program in return for a fee.
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
|
|
|
||||||
82
README.md
82
README.md
|
|
@ -6,18 +6,20 @@
|
||||||
|
|
||||||
[](https://hub.docker.com/r/convertx/convertx-cn)
|
[](https://hub.docker.com/r/convertx/convertx-cn)
|
||||||
[](https://github.com/pi-docket/ConvertX-CN/releases)
|
[](https://github.com/pi-docket/ConvertX-CN/releases)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 為什麼選擇 ConvertX-CN?
|
## 為什麼選擇 ConvertX-CN?
|
||||||
|
|
||||||
| 特色 | 說明 |
|
| 特色 | 說明 |
|
||||||
| ----------------- | ------------------------------------ |
|
| ----------------- | --------------------------------------- |
|
||||||
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
|
| 📁 **1000+ 格式** | 文件、圖片、影音、電子書一次搞定 |
|
||||||
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
|
| 🔧 **20+ 引擎** | LibreOffice、FFmpeg、Pandoc 全到位 |
|
||||||
| 🈶 **中文優化** | 內建中日韓字型與 OCR,告別亂碼 |
|
| 🈶 **中文優化** | 內建中日韓字型與 OCR,告別亂碼 |
|
||||||
| 🌐 **65 種語言** | 跨國團隊無障礙使用 |
|
| 🌐 **65 種語言** | 跨國團隊無障礙使用 |
|
||||||
| 📊 **PDF 翻譯** | 數學公式完整保留(PDFMathTranslate) |
|
| 📊 **PDF 翻譯** | PDFMathTranslate + BabelDOC 雙引擎 |
|
||||||
|
| 📄 **PDF 轉 MD** | MinerU 智能擷取(保留表格、公式、圖片) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -26,25 +28,42 @@
|
||||||
完整文件請參閱 **[文件中心](docs/README.md)**
|
完整文件請參閱 **[文件中心](docs/README.md)**
|
||||||
|
|
||||||
| 分類 | 連結 |
|
| 分類 | 連結 |
|
||||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| 🚀 快速入門 | [概覽](docs/getting-started/overview.md) · [快速開始](docs/getting-started/quick-start.md) · [FAQ](docs/getting-started/faq.md) |
|
| 🚀 快速入門 | [概覽](docs/快速入門/概覽.md) · [快速開始](docs/快速入門/快速開始.md) · [FAQ](docs/快速入門/常見問題.md) |
|
||||||
| 🐳 部署指南 | [Docker](docs/deployment/docker.md) · [反向代理](docs/deployment/reverse-proxy.md) |
|
| 🐳 部署指南 | [Docker](docs/部署指南/Docker.md) · [反向代理](docs/部署指南/反向代理.md) |
|
||||||
| ⚙️ 配置設定 | [環境變數](docs/configuration/environment-variables.md) · [安全性](docs/configuration/security.md) |
|
| ⚙️ 配置設定 | [環境變數](docs/配置設定/環境變數.md) · [安全性](docs/配置設定/安全性.md) |
|
||||||
| 🔌 功能說明 | [轉換器](docs/features/converters.md) · [OCR](docs/features/ocr.md) · [翻譯](docs/features/translation.md) |
|
| 🔌 功能說明 | [轉換器](docs/功能說明/轉換器.md) · [OCR](docs/功能說明/OCR.md) · [翻譯](docs/功能說明/翻譯.md) |
|
||||||
| 🔗 API | [API 總覽](docs/api/overview.md) · [端點說明](docs/api/endpoints.md) |
|
| 🔗 API | [API 總覽](docs/API/總覽.md) · [端點說明](docs/API/端點.md) |
|
||||||
| 👩💻 開發 | [專案結構](docs/development/project-structure.md) · [貢獻指南](docs/development/contribution.md) |
|
| 👩💻 開發 | [專案結構](docs/開發指南/專案結構.md) · [貢獻指南](docs/開發指南/貢獻指南.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 快速開始
|
## 🚀 快速開始
|
||||||
|
|
||||||
### Docker Compose(推薦)
|
### Docker Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 建立專案資料夾
|
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
|
||||||
mkdir -p ~/convertx-cn && cd ~/convertx-cn
|
docker run -d \
|
||||||
|
--name convertx-cn \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v ./data:/app/data \
|
||||||
|
-e TZ=Asia/Taipei \
|
||||||
|
-e JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0 \
|
||||||
|
convertx/convertx-cn:latest
|
||||||
|
```
|
||||||
|
|
||||||
# 2. 建立 docker-compose.yml
|
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
|
||||||
|
|
||||||
|
開啟瀏覽器:`http://localhost:3000`
|
||||||
|
|
||||||
|
### Docker Compose(推薦)
|
||||||
|
|
||||||
|
> 💡 以下命令會自動建立 `~/convertx-cn/data` 資料夾、產生 `docker-compose.yml` 並啟動服務
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn && \
|
||||||
cat > docker-compose.yml << 'EOF'
|
cat > docker-compose.yml << 'EOF'
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
|
|
@ -57,31 +76,16 @@ services:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Taipei
|
- TZ=Asia/Taipei
|
||||||
- JWT_SECRET=請更換為長且隨機的字串至少32字元
|
- JWT_SECRET=Xk9mPqL2vN7wR4tY6uI8oA3sD5fG1hJ0
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# 3. 啟動
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> ⚠️ **安全提醒**:正式環境請更換 `JWT_SECRET` 為自己的隨機字串(至少 32 字元)
|
||||||
|
|
||||||
開啟瀏覽器:`http://localhost:3000`
|
開啟瀏覽器:`http://localhost:3000`
|
||||||
|
|
||||||
> 📖 詳細說明請參閱 [快速開始](docs/getting-started/quick-start.md)
|
> 📖 詳細說明請參閱 [快速開始](docs/快速入門/快速開始.md)
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p ~/convertx-cn/data && cd ~/convertx-cn
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
--name convertx-cn \
|
|
||||||
--restart unless-stopped \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-v ./data:/app/data \
|
|
||||||
-e TZ=Asia/Taipei \
|
|
||||||
-e JWT_SECRET=請更換為長且隨機的字串 \
|
|
||||||
convertx/convertx-cn:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -94,7 +98,7 @@ docker run -d \
|
||||||
| 帳號 | admin@example.com |
|
| 帳號 | admin@example.com |
|
||||||
| 密碼 | admin |
|
| 密碼 | admin |
|
||||||
|
|
||||||
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案
|
> ⚠️ 示範站僅供測試,請勿上傳敏感檔案,會定期清理資料。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -106,14 +110,14 @@ docker run -d \
|
||||||
| 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 |
|
| 重啟後資料消失 | 確認 `./data:/app/data` 且資料夾存在 |
|
||||||
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
||||||
|
|
||||||
更多問題 → [FAQ](docs/getting-started/faq.md)
|
更多問題 → [FAQ](docs/快速入門/常見問題.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 支援格式
|
## 📦 支援格式
|
||||||
|
|
||||||
| 轉換器 | 用途 | 格式數 |
|
| 轉換器 | 用途 | 格式數 |
|
||||||
| ---------------- | -------- | ------ |
|
| ---------------- | --------------- | ------ |
|
||||||
| FFmpeg | 影音 | 400+ |
|
| FFmpeg | 影音 | 400+ |
|
||||||
| ImageMagick | 圖片 | 200+ |
|
| ImageMagick | 圖片 | 200+ |
|
||||||
| LibreOffice | 文件 | 60+ |
|
| LibreOffice | 文件 | 60+ |
|
||||||
|
|
@ -121,8 +125,10 @@ docker run -d \
|
||||||
| Calibre | 電子書 | 40+ |
|
| Calibre | 電子書 | 40+ |
|
||||||
| Inkscape | 向量圖 | 20+ |
|
| Inkscape | 向量圖 | 20+ |
|
||||||
| PDFMathTranslate | PDF 翻譯 | 15+ |
|
| PDFMathTranslate | PDF 翻譯 | 15+ |
|
||||||
|
| BabelDOC | PDF 翻譯/轉換 | 15+ |
|
||||||
|
| MinerU | PDF 轉 Markdown | 10+ |
|
||||||
|
|
||||||
完整列表 → [轉換器文件](docs/features/converters.md)
|
完整列表 → [轉換器文件](docs/功能說明/轉換器.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -144,4 +150,6 @@ docker compose up -d
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
[MIT](LICENSE) | 基於 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
|
This project is licensed under the **[GNU Affero General Public License v3.0 (AGPL-3.0)](LICENSE)**.
|
||||||
|
|
||||||
|
Based on [C4illin/ConvertX](https://github.com/C4illin/ConvertX).
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ Only the latest release is supported
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.
|
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/pi-docket/ConvertX-CN/security/advisories/new) tab.
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,6 @@ curl -X POST \
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [API 端點詳細說明](endpoints.md)
|
- [API 端點詳細說明](端點.md)
|
||||||
- [API 規格文件](../../api-server/docs/API_SPEC.md)
|
- [API 規格文件](../../api-server/docs/API_SPEC.md)
|
||||||
- [架構說明](../../api-server/docs/ARCHITECTURE.md)
|
- [架構說明](../../api-server/docs/ARCHITECTURE.md)
|
||||||
144
docs/README.md
144
docs/README.md
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
剛開始使用?從這裡開始:
|
剛開始使用?從這裡開始:
|
||||||
|
|
||||||
1. **[概覽](getting-started/overview.md)** — 了解 ConvertX-CN 是什麼
|
1. **[概覽](快速入門/概覽.md)** — 了解 ConvertX-CN 是什麼
|
||||||
2. **[快速開始](getting-started/quick-start.md)** — 5 分鐘內完成部署
|
2. **[快速開始](快速入門/快速開始.md)** — 5 分鐘內完成部署
|
||||||
3. **[常見問題](getting-started/faq.md)** — 解決常見問題
|
3. **[常見問題](快速入門/常見問題.md)** — 解決常見問題
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -19,13 +19,13 @@
|
||||||
適合一般使用者:
|
適合一般使用者:
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| ------------------------------------------ | -------------------- |
|
| ---------------------------------- | -------------------- |
|
||||||
| [快速開始](getting-started/quick-start.md) | 最快部署方式 |
|
| [快速開始](快速入門/快速開始.md) | 最快部署方式 |
|
||||||
| [支援的轉換器](features/converters.md) | 所有可用的轉換格式 |
|
| [支援的轉換器](功能說明/轉換器.md) | 所有可用的轉換格式 |
|
||||||
| [OCR 功能](features/ocr.md) | 光學字元辨識 |
|
| [OCR 功能](功能說明/OCR.md) | 光學字元辨識 |
|
||||||
| [翻譯功能](features/translation.md) | PDF 翻譯(保留公式) |
|
| [翻譯功能](功能說明/翻譯.md) | PDF 翻譯(保留公式) |
|
||||||
| [多語言介面](features/i18n.md) | 切換介面語言 |
|
| [多語言介面](功能說明/多語言.md) | 切換介面語言 |
|
||||||
| [常見問題](getting-started/faq.md) | FAQ |
|
| [常見問題](快速入門/常見問題.md) | FAQ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -36,18 +36,18 @@
|
||||||
### 部署
|
### 部署
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| --------------------------------------- | --------------------------- |
|
| --------------------------------- | --------------------------- |
|
||||||
| [Docker 部署](deployment/docker.md) | Docker Run & Docker Compose |
|
| [Docker 部署](部署指南/Docker.md) | Docker Run & Docker Compose |
|
||||||
| [反向代理](deployment/reverse-proxy.md) | Nginx / Traefik / Caddy |
|
| [反向代理](部署指南/反向代理.md) | Nginx / Traefik / Caddy |
|
||||||
| [範例配置](samples/README.md) | 可直接使用的配置檔 |
|
| [範例配置](範例/README.md) | 可直接使用的配置檔 |
|
||||||
|
|
||||||
### 配置
|
### 配置
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| -------------------------------------------------- | ------------------ |
|
| ------------------------------------ | ------------------ |
|
||||||
| [環境變數](configuration/environment-variables.md) | 所有可用設定 |
|
| [環境變數](配置設定/環境變數.md) | 所有可用設定 |
|
||||||
| [安全性設定](configuration/security.md) | HTTPS、認證、防護 |
|
| [安全性設定](配置設定/安全性.md) | HTTPS、認證、防護 |
|
||||||
| [清理與限制](configuration/limits-and-cleanup.md) | 自動清理、資源限制 |
|
| [清理與限制](配置設定/清理與限制.md) | 自動清理、資源限制 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -58,25 +58,25 @@
|
||||||
### 開發
|
### 開發
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| -------------------------------------------- | -------------- |
|
| -------------------------------- | -------------- |
|
||||||
| [專案結構](development/project-structure.md) | 程式碼結構說明 |
|
| [專案結構](開發指南/專案結構.md) | 程式碼結構說明 |
|
||||||
| [本地開發](development/local-development.md) | 開發環境設定 |
|
| [本地開發](開發指南/本地開發.md) | 開發環境設定 |
|
||||||
| [貢獻指南](development/contribution.md) | 如何參與專案 |
|
| [貢獻指南](開發指南/貢獻指南.md) | 如何參與專案 |
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| ---------------------------- | ------------------ |
|
| ----------------------- | ------------------ |
|
||||||
| [API 總覽](api/overview.md) | REST & GraphQL API |
|
| [API 總覽](API/總覽.md) | REST & GraphQL API |
|
||||||
| [API 端點](api/endpoints.md) | 詳細端點說明 |
|
| [API 端點](API/端點.md) | 詳細端點說明 |
|
||||||
|
|
||||||
### 測試
|
### 測試
|
||||||
|
|
||||||
| 文件 | 說明 |
|
| 文件 | 說明 |
|
||||||
| ------------------------------------ | -------------- |
|
| ---------------------------- | -------------- |
|
||||||
| [測試策略](testing/test-strategy.md) | 測試類型與方法 |
|
| [測試策略](測試/測試策略.md) | 測試類型與方法 |
|
||||||
| [CI/CD](testing/ci-cd.md) | 持續整合設定 |
|
| [CI/CD](測試/CI-CD.md) | 持續整合設定 |
|
||||||
| [E2E 測試](testing/e2e-tests.md) | 端對端測試 |
|
| [E2E 測試](測試/E2E測試.md) | 端對端測試 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -85,38 +85,38 @@
|
||||||
```
|
```
|
||||||
docs/
|
docs/
|
||||||
├── README.md ← 您在這裡
|
├── README.md ← 您在這裡
|
||||||
├── getting-started/ # 快速入門
|
├── 快速入門/
|
||||||
│ ├── overview.md # 概覽
|
│ ├── 概覽.md
|
||||||
│ ├── quick-start.md # 快速開始
|
│ ├── 快速開始.md
|
||||||
│ └── faq.md # 常見問題
|
│ └── 常見問題.md
|
||||||
├── deployment/ # 部署指南
|
├── 部署指南/
|
||||||
│ ├── docker.md # Docker 部署
|
│ ├── Docker.md
|
||||||
│ └── reverse-proxy.md # 反向代理
|
│ └── 反向代理.md
|
||||||
├── configuration/ # 配置設定
|
├── 配置設定/
|
||||||
│ ├── environment-variables.md # 環境變數
|
│ ├── 環境變數.md
|
||||||
│ ├── security.md # 安全性
|
│ ├── 安全性.md
|
||||||
│ └── limits-and-cleanup.md # 清理與限制
|
│ └── 清理與限制.md
|
||||||
├── features/ # 功能說明
|
├── 功能說明/
|
||||||
│ ├── converters.md # 轉換器
|
│ ├── 轉換器.md
|
||||||
│ ├── translation.md # 翻譯
|
│ ├── 翻譯.md
|
||||||
│ ├── ocr.md # OCR
|
│ ├── OCR.md
|
||||||
│ └── i18n.md # 多語言
|
│ └── 多語言.md
|
||||||
├── api/ # API 文件
|
├── API/
|
||||||
│ ├── overview.md # API 總覽
|
│ ├── 總覽.md
|
||||||
│ └── endpoints.md # API 端點
|
│ └── 端點.md
|
||||||
├── testing/ # 測試
|
├── 測試/
|
||||||
│ ├── test-strategy.md # 測試策略
|
│ ├── 測試策略.md
|
||||||
│ ├── ci-cd.md # CI/CD
|
│ ├── CI-CD.md
|
||||||
│ └── e2e-tests.md # E2E 測試
|
│ └── E2E測試.md
|
||||||
├── development/ # 開發指南
|
├── 開發指南/
|
||||||
│ ├── project-structure.md # 專案結構
|
│ ├── 專案結構.md
|
||||||
│ ├── local-development.md # 本地開發
|
│ ├── 本地開發.md
|
||||||
│ └── contribution.md # 貢獻指南
|
│ └── 貢獻指南.md
|
||||||
└── samples/ # 範例檔案
|
└── 範例/
|
||||||
├── compose.minimal.yml # 最小配置
|
├── compose.minimal.yml
|
||||||
├── compose.production.yml # 生產環境
|
├── compose.production.yml
|
||||||
├── compose.with-traefik.yml # Traefik 整合
|
├── compose.with-traefik.yml
|
||||||
└── nginx.example.conf # Nginx 設定
|
└── nginx.example.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -135,23 +135,23 @@ docs/
|
||||||
|
|
||||||
### 我是新手
|
### 我是新手
|
||||||
|
|
||||||
1. [概覽](getting-started/overview.md)
|
1. [概覽](快速入門/概覽.md)
|
||||||
2. [快速開始](getting-started/quick-start.md)
|
2. [快速開始](快速入門/快速開始.md)
|
||||||
3. [支援的轉換器](features/converters.md)
|
3. [支援的轉換器](功能說明/轉換器.md)
|
||||||
|
|
||||||
### 我要部署到生產環境
|
### 我要部署到生產環境
|
||||||
|
|
||||||
1. [Docker 部署](deployment/docker.md)
|
1. [Docker 部署](部署指南/Docker.md)
|
||||||
2. [反向代理](deployment/reverse-proxy.md)
|
2. [反向代理](部署指南/反向代理.md)
|
||||||
3. [安全性設定](configuration/security.md)
|
3. [安全性設定](配置設定/安全性.md)
|
||||||
4. [環境變數](configuration/environment-variables.md)
|
4. [環境變數](配置設定/環境變數.md)
|
||||||
|
|
||||||
### 我想參與開發
|
### 我想參與開發
|
||||||
|
|
||||||
1. [專案結構](development/project-structure.md)
|
1. [專案結構](開發指南/專案結構.md)
|
||||||
2. [本地開發](development/local-development.md)
|
2. [本地開發](開發指南/本地開發.md)
|
||||||
3. [貢獻指南](development/contribution.md)
|
3. [貢獻指南](開發指南/貢獻指南.md)
|
||||||
4. [測試策略](testing/test-strategy.md)
|
4. [測試策略](測試/測試策略.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🐳 [Docker 部署(含硬體加速)](deployment/docker.md)
|
> - 🐳 [Docker 部署(含硬體加速)](部署指南/Docker.md)
|
||||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
|
> - 🔧 [反向代理設定](部署指南/反向代理.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - ⚙️ [環境變數設定](../configuration/environment-variables.md)
|
> - ⚙️ [環境變數設定](../配置設定/環境變數.md)
|
||||||
> - 🔒 [安全性設定](../configuration/security.md)
|
> - 🔒 [安全性設定](../配置設定/安全性.md)
|
||||||
> - 🧹 [清理與限制](../configuration/limits-and-cleanup.md)
|
> - 🧹 [清理與限制](../配置設定/清理與限制.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🔒 [安全性設定](../configuration/security.md)
|
> - 🔒 [安全性設定](../配置設定/安全性.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🔌 [轉換器完整說明](features/converters.md)
|
> - 🔌 [轉換器完整說明](功能說明/轉換器.md)
|
||||||
> - 📊 [OCR 功能](features/ocr.md)
|
> - 📊 [OCR 功能](功能說明/OCR.md)
|
||||||
> - 🌐 [翻譯功能](features/translation.md)
|
> - 🌐 [翻譯功能](功能說明/翻譯.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🐳 [Docker 部署](deployment/docker.md)
|
> - 🐳 [Docker 部署](部署指南/Docker.md)
|
||||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.md)
|
> - 🔧 [反向代理設定](部署指南/反向代理.md)
|
||||||
> - 🔒 [安全性設定](configuration/security.md)
|
> - 🔒 [安全性設定](配置設定/安全性.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 📦 [Docker 部署指南](deployment/docker.md)
|
> - 📦 [Docker 部署指南](部署指南/Docker.md)
|
||||||
> - 🔧 [反向代理設定](deployment/reverse-proxy.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)
|
> - 📖 [概覽](快速入門/概覽.md)
|
||||||
> - 🚀 [快速開始](getting-started/quick-start.md)
|
> - 🚀 [快速開始](快速入門/快速開始.md)
|
||||||
> - ❓ [常見問題](getting-started/faq.md)
|
> - ❓ [常見問題](快速入門/常見問題.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🌐 [多語言介面支援](features/i18n.md)
|
> - 🌐 [多語言介面支援](功能說明/多語言.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
>
|
>
|
||||||
> 本文件內容已整合至新的文件結構,請參閱:
|
> 本文件內容已整合至新的文件結構,請參閱:
|
||||||
>
|
>
|
||||||
> - 🛠️ [專案結構](development/project-structure.md)
|
> - 🛠️ [專案結構](開發指南/專案結構.md)
|
||||||
> - 🐳 [Docker 部署](deployment/docker.md)
|
> - 🐳 [Docker 部署](部署指南/Docker.md)
|
||||||
>
|
>
|
||||||
> 此文件將在未來版本中移除。
|
> 此文件將在未來版本中移除。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
# OCR 功能
|
# OCR 功能
|
||||||
|
|
||||||
ConvertX-CN 內建 Tesseract OCR,可將圖片中的文字轉換為可編輯文字。
|
ConvertX-CN 內建 Tesseract OCR 與 ocrmypdf,提供完整的 OCR 功能:
|
||||||
|
|
||||||
|
- **圖片 OCR**:將圖片中的文字轉換為可編輯文字
|
||||||
|
- **PDF OCR**:將掃描版 PDF 轉換為可搜尋 PDF(加入文字層)
|
||||||
|
- **PDF 自動偵測**:翻譯引擎會自動偵測掃描版 PDF 並進行 OCR 處理
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -22,24 +26,51 @@ ConvertX-CN 完整版內建以下 OCR 語言:
|
||||||
|
|
||||||
## 使用方式
|
## 使用方式
|
||||||
|
|
||||||
|
### PDF → 可搜尋 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 等)
|
1. 上傳圖片(PNG, JPG, TIFF 等)
|
||||||
2. 選擇目標格式 `txt` 或 `pdf`(可搜尋)
|
2. 選擇目標格式 `txt` 或 `pdf`(可搜尋)
|
||||||
3. 進行轉換
|
3. 進行轉換
|
||||||
|
|
||||||
### PDF → 可搜尋 PDF
|
### 掃描版 PDF 翻譯(自動處理)
|
||||||
|
|
||||||
1. 上傳掃描版 PDF
|
當使用 PDFMathTranslate 或 BabelDOC 翻譯 PDF 時:
|
||||||
2. 選擇 OCR 處理
|
|
||||||
3. 獲得可搜尋的 PDF
|
1. 系統會**自動偵測**是否為掃描版 PDF(無文字層)
|
||||||
|
2. 若為掃描版,系統會**自動執行 OCR** 加入文字層
|
||||||
|
3. 翻譯引擎使用 OCR 處理後的 PDF 進行翻譯
|
||||||
|
4. 使用者無需手動操作,全程自動完成
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 支援的輸入格式
|
## 支援的輸入格式
|
||||||
|
|
||||||
- **點陣圖**:PNG, JPG, JPEG, TIFF, BMP, GIF
|
- **點陣圖**:PNG, JPG, JPEG, TIFF, BMP, GIF
|
||||||
- **文件**:PDF(掃描版)
|
|
||||||
- **其他**:WebP, PNM, PBM
|
- **其他**:WebP, PNM, PBM
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -78,14 +109,18 @@ Tesseract 可同時辨識多種語言,但準確度可能下降。
|
||||||
|
|
||||||
### 方法一:自訂 Dockerfile
|
### 方法一:自訂 Dockerfile
|
||||||
|
|
||||||
|
> 💡 在 `Dockerfile.full` 中取消註解以下內容
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
# 在 Dockerfile.full 中取消註解
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
tesseract-ocr-spa \ # 西班牙文
|
tesseract-ocr-spa \
|
||||||
tesseract-ocr-ita \ # 義大利文
|
tesseract-ocr-ita \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `tesseract-ocr-spa` — 西班牙文
|
||||||
|
- `tesseract-ocr-ita` — 義大利文
|
||||||
|
|
||||||
### 方法二:掛載語言包
|
### 方法二:掛載語言包
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -112,6 +147,6 @@ volumes:
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [支援的轉換器](converters.md)
|
- [支援的轉換器](轉換器.md)
|
||||||
- [翻譯功能](translation.md)
|
- [翻譯功能](翻譯.md)
|
||||||
- [Docker 部署](../deployment/docker.md)
|
- [Docker 部署](../部署指南/Docker.md)
|
||||||
|
|
@ -128,5 +128,5 @@ http://localhost:3000/?lang=ja
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [貢獻指南](../development/contribution.md)
|
- [貢獻指南](../開發指南/貢獻指南.md)
|
||||||
- [專案結構](../development/project-structure.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: 支援哪些格式?
|
### Q: 支援哪些格式?
|
||||||
|
|
||||||
1000+ 種格式,詳見 [支援的轉換器](../features/converters.md)
|
1000+ 種格式,詳見 [支援的轉換器](../功能說明/轉換器.md)
|
||||||
|
|
||||||
### Q: 轉換失敗怎麼辦?
|
### Q: 轉換失敗怎麼辦?
|
||||||
|
|
||||||
|
|
@ -169,11 +169,11 @@ client_max_body_size 500M;
|
||||||
|
|
||||||
### Q: 如何使用反向代理?
|
### Q: 如何使用反向代理?
|
||||||
|
|
||||||
詳見 [反向代理設定](../deployment/reverse-proxy.md)
|
詳見 [反向代理設定](../部署指南/反向代理.md)
|
||||||
|
|
||||||
### Q: 如何啟用硬體加速?
|
### Q: 如何啟用硬體加速?
|
||||||
|
|
||||||
詳見 [進階配置 - 硬體加速](../deployment/docker.md#硬體加速)
|
詳見 [進階配置 - 硬體加速](../部署指南/Docker.md#硬體加速)
|
||||||
|
|
||||||
### Q: 如何啟用 API Server?
|
### Q: 如何啟用 API Server?
|
||||||
|
|
||||||
|
|
@ -181,7 +181,7 @@ client_max_body_size 500M;
|
||||||
docker compose --profile api up -d
|
docker compose --profile api up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
詳見 [API 文件](../api/overview.md)
|
詳見 [API 文件](../API/總覽.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -138,7 +138,7 @@ environment:
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- 📖 [Docker 詳細配置](../deployment/docker.md)
|
- 📖 [Docker 詳細配置](../部署指南/Docker.md)
|
||||||
- ⚙️ [環境變數設定](../configuration/environment-variables.md)
|
- ⚙️ [環境變數設定](../配置設定/環境變數.md)
|
||||||
- 🔒 [安全性設定](../configuration/security.md)
|
- 🔒 [安全性設定](../配置設定/安全性.md)
|
||||||
- 🔧 [反向代理設定](../deployment/reverse-proxy.md)
|
- 🔧 [反向代理設定](../部署指南/反向代理.md)
|
||||||
|
|
@ -78,6 +78,6 @@ ConvertX-CN 是 fork 自 [C4illin/ConvertX](https://github.com/C4illin/ConvertX)
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- 🚀 [快速開始](quick-start.md) — 5 分鐘內完成部署
|
- 🚀 [快速開始](快速開始.md) — 5 分鐘內完成部署
|
||||||
- ❓ [常見問題](faq.md) — 解決常見問題
|
- ❓ [常見問題](常見問題.md) — 解決常見問題
|
||||||
- 📦 [Docker 部署](../deployment/docker.md) — 詳細部署指南
|
- 📦 [Docker 部署](../部署指南/Docker.md) — 詳細部署指南
|
||||||
|
|
@ -143,6 +143,6 @@ docker build -t convertx-cn-test .
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [測試策略](test-strategy.md)
|
- [測試策略](測試策略.md)
|
||||||
- [E2E 測試](e2e-tests.md)
|
- [E2E 測試](E2E測試.md)
|
||||||
- [貢獻指南](../development/contribution.md)
|
- [貢獻指南](../開發指南/貢獻指南.md)
|
||||||
|
|
@ -176,6 +176,6 @@ npx playwright codegen http://localhost:3000
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [測試策略](test-strategy.md)
|
- [測試策略](測試策略.md)
|
||||||
- [CI/CD](ci-cd.md)
|
- [CI/CD](CI-CD.md)
|
||||||
- [本地開發](../development/local-development.md)
|
- [本地開發](../開發指南/本地開發.md)
|
||||||
|
|
@ -49,6 +49,6 @@ docker compose up -d
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [Docker 部署](../deployment/docker.md)
|
- [Docker 部署](../部署指南/Docker.md)
|
||||||
- [環境變數](../configuration/environment-variables.md)
|
- [環境變數](../配置設定/環境變數.md)
|
||||||
- [反向代理](../deployment/reverse-proxy.md)
|
- [反向代理](../部署指南/反向代理.md)
|
||||||
|
|
@ -97,21 +97,29 @@ docker run -d \
|
||||||
|
|
||||||
**重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。
|
**重要**:請務必先建立資料夾,否則 Docker 會建立匿名 volume。
|
||||||
|
|
||||||
```bash
|
**Linux / macOS:**
|
||||||
# Linux / macOS
|
|
||||||
mkdir -p ~/convertx-cn/data
|
|
||||||
|
|
||||||
# Windows PowerShell
|
```bash
|
||||||
|
mkdir -p ~/convertx-cn/data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows PowerShell:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
mkdir C:\convertx-cn\data
|
mkdir C:\convertx-cn\data
|
||||||
```
|
```
|
||||||
|
|
||||||
### 備份與還原
|
### 備份與還原
|
||||||
|
|
||||||
```bash
|
**備份:**
|
||||||
# 備份
|
|
||||||
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
|
|
||||||
|
|
||||||
# 還原
|
```bash
|
||||||
|
tar -czvf convertx-backup-$(date +%Y%m%d).tar.gz ./data
|
||||||
|
```
|
||||||
|
|
||||||
|
**還原:**
|
||||||
|
|
||||||
|
```bash
|
||||||
tar -xzvf convertx-backup-20260120.tar.gz
|
tar -xzvf convertx-backup-20260120.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -199,18 +207,23 @@ services:
|
||||||
|
|
||||||
## 版本更新
|
## 版本更新
|
||||||
|
|
||||||
```bash
|
**1. 拉取最新版本:**
|
||||||
# 拉取最新版本
|
|
||||||
docker pull convertx/convertx-cn:latest
|
|
||||||
|
|
||||||
# 停止並移除舊容器
|
```bash
|
||||||
|
docker pull convertx/convertx-cn:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 停止並移除舊容器:**
|
||||||
|
|
||||||
|
```bash
|
||||||
docker stop convertx-cn
|
docker stop convertx-cn
|
||||||
docker rm convertx-cn
|
docker rm convertx-cn
|
||||||
|
```
|
||||||
|
|
||||||
# 重新啟動(使用相同的參數)
|
**3. 重新啟動(使用相同的參數):**
|
||||||
docker run -d \
|
|
||||||
--name convertx-cn \
|
```bash
|
||||||
# ... 其他參數
|
docker run -d --name convertx-cn ...
|
||||||
```
|
```
|
||||||
|
|
||||||
或使用 Docker Compose:
|
或使用 Docker Compose:
|
||||||
|
|
@ -228,7 +241,12 @@ docker compose up -d
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker logs convertx-cn
|
docker logs convertx-cn
|
||||||
docker logs -f convertx-cn # 持續追蹤
|
```
|
||||||
|
|
||||||
|
持續追蹤日誌:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f convertx-cn
|
||||||
```
|
```
|
||||||
|
|
||||||
### 進入容器
|
### 進入容器
|
||||||
|
|
@ -251,5 +269,5 @@ docker exec -it convertx-cn /bin/bash
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [Docker Compose 詳解](docker-compose.md)
|
- [Docker Compose 詳解](docker-compose.md)
|
||||||
- [反向代理設定](reverse-proxy.md)
|
- [反向代理設定](反向代理.md)
|
||||||
- [環境變數設定](../configuration/environment-variables.md)
|
- [環境變數設定](../配置設定/環境變數.md)
|
||||||
|
|
@ -111,7 +111,7 @@ environment:
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24
|
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||||
```
|
```
|
||||||
|
|
||||||
完整環境變數說明請參考 [環境變數文件](../config/environment.md)。
|
完整環境變數說明請參考 [環境變數文件](../配置設定/環境變數.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -59,6 +59,8 @@ cd C:\convertx-cn
|
||||||
|
|
||||||
在專案資料夾建立 `docker-compose.yml`:
|
在專案資料夾建立 `docker-compose.yml`:
|
||||||
|
|
||||||
|
> 💡 `JWT_SECRET` 請改成你自己的隨機字串(至少 32 字元)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
|
|
@ -71,7 +73,7 @@ services:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Taipei
|
- TZ=Asia/Taipei
|
||||||
- JWT_SECRET=請改成你自己的隨機字串至少32字元
|
- JWT_SECRET=your-random-secret-at-least-32-chars
|
||||||
```
|
```
|
||||||
|
|
||||||
### 必要參數說明
|
### 必要參數說明
|
||||||
|
|
@ -119,12 +121,12 @@ docker compose logs -f # Ctrl+C 退出
|
||||||
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
| 重啟後被登出 | 設定固定的 `JWT_SECRET` |
|
||||||
| 3000 埠被佔用 | 改用 `"8080:3000"` |
|
| 3000 埠被佔用 | 改用 `"8080:3000"` |
|
||||||
|
|
||||||
更多問題請參考 [FAQ](../faq.md)。
|
更多問題請參考 [FAQ](../快速入門/常見問題.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- [環境變數完整說明](../config/environment.md)
|
- [環境變數完整說明](../配置設定/環境變數.md)
|
||||||
- [反向代理與 HTTPS](../deployment.md)
|
- [反向代理與 HTTPS](反向代理.md)
|
||||||
- [版本更新方法](update.md)
|
- [版本更新方法](update.md)
|
||||||
|
|
@ -24,12 +24,12 @@ docker compose up -d
|
||||||
|
|
||||||
### 更新到指定版本
|
### 更新到指定版本
|
||||||
|
|
||||||
修改 `docker-compose.yml`:
|
修改 `docker-compose.yml`(指定要更新的版本號):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
image: convertx/convertx-cn:v0.1.9 # 指定版本
|
image: convertx/convertx-cn:v0.1.9
|
||||||
```
|
```
|
||||||
|
|
||||||
然後執行:
|
然後執行:
|
||||||
|
|
@ -78,12 +78,12 @@ Copy-Item -Recurse .\data .\data.backup.$(Get-Date -Format "yyyyMMdd")
|
||||||
|
|
||||||
## 回滾版本
|
## 回滾版本
|
||||||
|
|
||||||
如果新版本有問題,可以回滾到舊版本:
|
如果新版本有問題,可以回滾到舊版本(將版本號改為舊版本):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
image: convertx/convertx-cn:v0.1.8 # 舊版本
|
image: convertx/convertx-cn:v0.1.8
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -120,6 +120,8 @@ docker rmi convertx/convertx-cn:v0.1.7
|
||||||
|
|
||||||
可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器:
|
可使用 [Watchtower](https://containrrr.dev/watchtower/) 自動更新容器:
|
||||||
|
|
||||||
|
> 💡 `--interval 86400` — 每 24 小時檢查一次更新
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
|
|
@ -130,7 +132,7 @@ services:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
command: --interval 86400 # 每 24 小時檢查一次
|
command: --interval 86400
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ 自動更新適合測試環境。生產環境建議手動更新,確認新版本穩定後再升級。
|
> ⚠️ 自動更新適合測試環境。生產環境建議手動更新,確認新版本穩定後再升級。
|
||||||
|
|
@ -8,10 +8,13 @@
|
||||||
|
|
||||||
透過反向代理存取時,請設定:
|
透過反向代理存取時,請設定:
|
||||||
|
|
||||||
|
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
|
||||||
|
- `HTTP_ALLOWED=false` — Proxy 已處理 HTTPS
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
|
- TRUST_PROXY=true
|
||||||
- HTTP_ALLOWED=false # Proxy 已處理 HTTPS
|
- HTTP_ALLOWED=false
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -10,9 +10,11 @@
|
||||||
|
|
||||||
控制是否允許非 HTTPS 連線。
|
控制是否允許非 HTTPS 連線。
|
||||||
|
|
||||||
|
- `HTTP_ALLOWED=true` — 允許 HTTP(不安全,僅測試用)
|
||||||
|
- `HTTP_ALLOWED=false` — 僅允許 HTTPS(預設)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- HTTP_ALLOWED=true # 允許 HTTP(不安全,僅測試用)
|
- HTTP_ALLOWED=false
|
||||||
- HTTP_ALLOWED=false # 僅允許 HTTPS(預設)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 運作原理
|
#### 運作原理
|
||||||
|
|
@ -40,9 +42,11 @@
|
||||||
|
|
||||||
是否信任反向代理傳來的 headers。
|
是否信任反向代理傳來的 headers。
|
||||||
|
|
||||||
|
- `TRUST_PROXY=true` — 信任 X-Forwarded-\* headers
|
||||||
|
- `TRUST_PROXY=false` — 不信任(預設)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- TRUST_PROXY=true # 信任 X-Forwarded-* headers
|
- TRUST_PROXY=false
|
||||||
- TRUST_PROXY=false # 不信任(預設)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 運作原理
|
#### 運作原理
|
||||||
|
|
@ -69,9 +73,11 @@
|
||||||
|
|
||||||
### ACCOUNT_REGISTRATION
|
### ACCOUNT_REGISTRATION
|
||||||
|
|
||||||
|
- `ACCOUNT_REGISTRATION=true` — 開放註冊
|
||||||
|
- `ACCOUNT_REGISTRATION=false` — 關閉註冊
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ACCOUNT_REGISTRATION=true # 開放註冊
|
- ACCOUNT_REGISTRATION=false
|
||||||
- ACCOUNT_REGISTRATION=false # 關閉註冊
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 建議流程
|
#### 建議流程
|
||||||
|
|
@ -95,22 +101,41 @@
|
||||||
|
|
||||||
#### 產生方式
|
#### 產生方式
|
||||||
|
|
||||||
```bash
|
**Linux / macOS:**
|
||||||
# Linux / macOS
|
|
||||||
openssl rand -hex 32
|
|
||||||
|
|
||||||
# 輸出範例:a1b2c3d4e5f6789...(64 字元)
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Windows PowerShell:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
|
||||||
|
```
|
||||||
|
|
||||||
|
**線上工具:**
|
||||||
|
|
||||||
|
- https://generate-secret.vercel.app/32
|
||||||
|
|
||||||
|
**Node.js:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 產生的密鑰應為 32-64 字元的隨機字串,例如:`a1b2c3d4e5f6789...`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 公開服務安全
|
## 公開服務安全
|
||||||
|
|
||||||
### ALLOW_UNAUTHENTICATED
|
### ALLOW_UNAUTHENTICATED
|
||||||
|
|
||||||
|
- `ALLOW_UNAUTHENTICATED=true` — 允許未登入使用
|
||||||
|
- `ALLOW_UNAUTHENTICATED=false` — 必須登入(預設)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- ALLOW_UNAUTHENTICATED=true # 允許未登入使用
|
- ALLOW_UNAUTHENTICATED=false
|
||||||
- ALLOW_UNAUTHENTICATED=false # 必須登入(預設)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 風險
|
#### 風險
|
||||||
|
|
@ -125,12 +150,16 @@ openssl rand -hex 32
|
||||||
|
|
||||||
若需要公開服務,建議:
|
若需要公開服務,建議:
|
||||||
|
|
||||||
|
- `AUTO_DELETE_EVERY_N_HOURS=1` — 頻繁清理
|
||||||
|
- `HIDE_HISTORY=true` — 隱藏歷史
|
||||||
|
- `MAX_CONVERT_PROCESS=2` — 限制同時轉換數
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- ALLOW_UNAUTHENTICATED=true
|
- ALLOW_UNAUTHENTICATED=true
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=1 # 頻繁清理
|
- AUTO_DELETE_EVERY_N_HOURS=1
|
||||||
- HIDE_HISTORY=true # 隱藏歷史
|
- HIDE_HISTORY=true
|
||||||
- MAX_CONVERT_PROCESS=2 # 限制同時轉換數
|
- MAX_CONVERT_PROCESS=2
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -139,31 +168,36 @@ environment:
|
||||||
|
|
||||||
### 防火牆
|
### 防火牆
|
||||||
|
|
||||||
只開放必要的埠:
|
只開放必要的埠。
|
||||||
|
|
||||||
|
**UFW (Ubuntu) 開放單一埠:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# UFW (Ubuntu)
|
|
||||||
sudo ufw allow 3000/tcp
|
sudo ufw allow 3000/tcp
|
||||||
|
```
|
||||||
|
|
||||||
# 或只允許特定 IP
|
**只允許特定 IP 範圍:**
|
||||||
|
|
||||||
|
```bash
|
||||||
sudo ufw allow from 192.168.1.0/24 to any port 3000
|
sudo ufw allow from 192.168.1.0/24 to any port 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### 只允許本機存取
|
### 只允許本機存取
|
||||||
|
|
||||||
|
> 💡 `127.0.0.1:3000:3000` 只有本機可存取
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:3000:3000" # 只有本機可存取
|
- "127.0.0.1:3000:3000"
|
||||||
```
|
```
|
||||||
|
|
||||||
搭配反向代理提供對外服務。
|
搭配反向代理提供對外服務。
|
||||||
|
|
||||||
### 限制上傳大小
|
### 限制上傳大小
|
||||||
|
|
||||||
在反向代理層限制:
|
在反向代理層限制(Nginx 範例):
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# Nginx
|
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -173,21 +207,25 @@ client_max_body_size 100M;
|
||||||
|
|
||||||
### 定期清理
|
### 定期清理
|
||||||
|
|
||||||
|
> 💡 每 24 小時自動清理過期檔案
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24 # 每 24 小時清理
|
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||||
```
|
```
|
||||||
|
|
||||||
### 備份
|
### 備份
|
||||||
|
|
||||||
|
使用 crontab 設定定期備份(每天凌晨 2 點):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 定期備份資料
|
|
||||||
0 2 * * * tar -czvf /backup/convertx-$(date +\%Y\%m\%d).tar.gz /path/to/data
|
0 2 * * * tar -czvf /backup/convertx-$(date +\%Y\%m\%d).tar.gz /path/to/data
|
||||||
```
|
```
|
||||||
|
|
||||||
### 權限設定
|
### 權限設定
|
||||||
|
|
||||||
|
限制資料夾權限,僅允許擁有者存取:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 限制資料夾權限
|
|
||||||
chmod 700 ./data
|
chmod 700 ./data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -214,6 +252,6 @@ chmod 700 ./data
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [環境變數設定](environment-variables.md)
|
- [環境變數設定](環境變數.md)
|
||||||
- [反向代理設定](../deployment/reverse-proxy.md)
|
- [反向代理設定](../部署指南/反向代理.md)
|
||||||
- [Docker 部署](../deployment/docker.md)
|
- [Docker 部署](../部署指南/Docker.md)
|
||||||
|
|
@ -16,16 +16,13 @@
|
||||||
| 停用 | `0` |
|
| 停用 | `0` |
|
||||||
| 單位 | 小時 |
|
| 單位 | 小時 |
|
||||||
|
|
||||||
|
- `AUTO_DELETE_EVERY_N_HOURS=24` — 每 24 小時清理一次
|
||||||
|
- `AUTO_DELETE_EVERY_N_HOURS=1` — 每 1 小時清理一次(公開服務建議)
|
||||||
|
- `AUTO_DELETE_EVERY_N_HOURS=0` — 停用自動清理
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
# 每 24 小時清理一次
|
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24
|
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||||
|
|
||||||
# 每 1 小時清理一次(公開服務建議)
|
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=1
|
|
||||||
|
|
||||||
# 停用自動清理
|
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 清理範圍
|
### 清理範圍
|
||||||
|
|
@ -49,13 +46,12 @@ environment:
|
||||||
| ------ | ------------- |
|
| ------ | ------------- |
|
||||||
| 預設值 | `0`(無限制) |
|
| 預設值 | `0`(無限制) |
|
||||||
|
|
||||||
|
- `MAX_CONVERT_PROCESS=2` — 最多同時 2 個轉換任務
|
||||||
|
- `MAX_CONVERT_PROCESS=0` — 無限制
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
# 最多同時 2 個轉換任務
|
|
||||||
- MAX_CONVERT_PROCESS=2
|
- MAX_CONVERT_PROCESS=2
|
||||||
|
|
||||||
# 無限制
|
|
||||||
- MAX_CONVERT_PROCESS=0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 使用建議
|
#### 使用建議
|
||||||
|
|
@ -152,17 +148,22 @@ find ./data/output -mtime +7 -delete
|
||||||
|
|
||||||
### 個人使用
|
### 個人使用
|
||||||
|
|
||||||
|
- `AUTO_DELETE_EVERY_N_HOURS=168` — 一週清理一次
|
||||||
|
- `MAX_CONVERT_PROCESS=0` — 無限制
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=168 # 一週
|
- AUTO_DELETE_EVERY_N_HOURS=168
|
||||||
- MAX_CONVERT_PROCESS=0 # 無限制
|
- MAX_CONVERT_PROCESS=0
|
||||||
```
|
```
|
||||||
|
|
||||||
### 小型團隊
|
### 小型團隊
|
||||||
|
|
||||||
|
> 💡 `AUTO_DELETE_EVERY_N_HOURS=24` — 每天清理一次
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24 # 一天
|
- AUTO_DELETE_EVERY_N_HOURS=24
|
||||||
- MAX_CONVERT_PROCESS=4
|
- MAX_CONVERT_PROCESS=4
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
|
@ -173,9 +174,11 @@ deploy:
|
||||||
|
|
||||||
### 公開服務
|
### 公開服務
|
||||||
|
|
||||||
|
> 💡 `AUTO_DELETE_EVERY_N_HOURS=1` — 每小時清理一次
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=1 # 一小時
|
- AUTO_DELETE_EVERY_N_HOURS=1
|
||||||
- MAX_CONVERT_PROCESS=2
|
- MAX_CONVERT_PROCESS=2
|
||||||
- ALLOW_UNAUTHENTICATED=true
|
- ALLOW_UNAUTHENTICATED=true
|
||||||
- HIDE_HISTORY=true
|
- HIDE_HISTORY=true
|
||||||
|
|
@ -191,6 +194,6 @@ deploy:
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [環境變數設定](environment-variables.md)
|
- [環境變數設定](環境變數.md)
|
||||||
- [安全性設定](security.md)
|
- [安全性設定](安全性.md)
|
||||||
- [Docker 部署](../deployment/docker.md)
|
- [Docker 部署](../部署指南/Docker.md)
|
||||||
|
|
@ -174,6 +174,6 @@ api-server/
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [本地開發](local-development.md)
|
- [本地開發](本地開發.md)
|
||||||
- [貢獻指南](contribution.md)
|
- [貢獻指南](貢獻指南.md)
|
||||||
- [測試策略](../testing/test-strategy.md)
|
- [測試策略](../測試/測試策略.md)
|
||||||
|
|
@ -187,6 +187,6 @@ docs: update deployment guide
|
||||||
|
|
||||||
## 相關文件
|
## 相關文件
|
||||||
|
|
||||||
- [專案結構](project-structure.md)
|
- [專案結構](專案結構.md)
|
||||||
- [本地開發](local-development.md)
|
- [本地開發](本地開發.md)
|
||||||
- [測試策略](../testing/test-strategy.md)
|
- [測試策略](../測試/測試策略.md)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "convertx-frontend",
|
"name": "convertx-frontend",
|
||||||
"version": "0.1.13",
|
"version": "0.1.14",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --watch src/index.tsx",
|
"dev": "bun run --watch src/index.tsx",
|
||||||
"hot": "bun run --hot src/index.tsx",
|
"hot": "bun run --hot src/index.tsx",
|
||||||
|
|
@ -60,5 +60,6 @@
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
"@tailwindcss/oxide",
|
"@tailwindcss/oxide",
|
||||||
"oxc-resolver"
|
"oxc-resolver"
|
||||||
]
|
],
|
||||||
|
"license": "AGPL-3.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { join, basename, dirname } from "node:path";
|
import { join, basename, dirname } from "node:path";
|
||||||
import { ExecFileFn } from "./types";
|
import { ExecFileFn } from "./types";
|
||||||
import { getArchiveFileName } from "../transfer";
|
import { getArchiveFileName } from "../transfer";
|
||||||
|
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BabelDOC Content Engine
|
* BabelDOC Content Engine
|
||||||
|
|
@ -275,7 +276,19 @@ export async function convert(
|
||||||
_options?: unknown,
|
_options?: unknown,
|
||||||
execFile: ExecFileFn = execFileOriginal,
|
execFile: ExecFileFn = execFileOriginal,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let ocrTempFile: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
|
||||||
|
console.log(`[BabelDOC] Checking if PDF needs OCR...`);
|
||||||
|
const ocrResult = await ensureSearchablePdf(filePath, execFile);
|
||||||
|
const inputPdf = ocrResult.path;
|
||||||
|
ocrTempFile = ocrResult.tempFile;
|
||||||
|
|
||||||
|
if (ocrResult.wasOcred) {
|
||||||
|
console.log(`[BabelDOC] ✅ Scanned PDF detected and OCR'd automatically`);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 檢查資源(警告但不阻止)
|
// 1. 檢查資源(警告但不阻止)
|
||||||
checkResourcesExist();
|
checkResourcesExist();
|
||||||
|
|
||||||
|
|
@ -300,8 +313,8 @@ export async function convert(
|
||||||
// 5. 設定 BabelDOC 輸出路徑(依輸出格式決定副檔名)
|
// 5. 設定 BabelDOC 輸出路徑(依輸出格式決定副檔名)
|
||||||
const translatedFilePath = join(tempDir, `${inputFileName}-translated.${outputExt}`);
|
const translatedFilePath = join(tempDir, `${inputFileName}-translated.${outputExt}`);
|
||||||
|
|
||||||
// 6. 執行 babeldoc 翻譯
|
// 6. 執行 babeldoc 翻譯(使用 OCR 處理後的 PDF)
|
||||||
await runBabelDoc(filePath, translatedFilePath, targetLang, outputFormat, execFile);
|
await runBabelDoc(inputPdf, translatedFilePath, targetLang, outputFormat, execFile);
|
||||||
|
|
||||||
// 7. 複製翻譯後的檔案到封裝目錄
|
// 7. 複製翻譯後的檔案到封裝目錄
|
||||||
const translatedDest = join(archiveDir, `translated-${targetLang}.${outputExt}`);
|
const translatedDest = join(archiveDir, `translated-${targetLang}.${outputExt}`);
|
||||||
|
|
@ -345,8 +358,13 @@ export async function convert(
|
||||||
// 10. 清理臨時目錄
|
// 10. 清理臨時目錄
|
||||||
removeDir(tempDir);
|
removeDir(tempDir);
|
||||||
|
|
||||||
|
// 11. 清理 OCR 暫存檔案
|
||||||
|
cleanupOcrTempFile(ocrTempFile);
|
||||||
|
|
||||||
return "Done";
|
return "Done";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 確保清理 OCR 暫存檔案
|
||||||
|
cleanupOcrTempFile(ocrTempFile);
|
||||||
throw new Error(`BabelDOC error: ${error}`);
|
throw new Error(`BabelDOC error: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
properties as propertiesPDFMathTranslate,
|
properties as propertiesPDFMathTranslate,
|
||||||
} from "./pdfmathtranslate";
|
} from "./pdfmathtranslate";
|
||||||
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
|
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
|
||||||
|
import { convert as convertOcrMyPdf, properties as propertiesOcrMyPdf } from "./ocrmypdf";
|
||||||
|
|
||||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||||
|
|
||||||
|
|
@ -156,6 +157,10 @@ const properties: Record<
|
||||||
properties: propertiesBabelDoc,
|
properties: propertiesBabelDoc,
|
||||||
converter: convertBabelDoc,
|
converter: convertBabelDoc,
|
||||||
},
|
},
|
||||||
|
OCRmyPDF: {
|
||||||
|
properties: propertiesOcrMyPdf,
|
||||||
|
converter: convertOcrMyPdf,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function chunks<T>(arr: T[], size: number): T[][] {
|
function chunks<T>(arr: T[], size: number): T[][] {
|
||||||
|
|
|
||||||
271
src/converters/ocrmypdf.ts
Normal file
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 { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, copyFileSync } from "node:fs";
|
||||||
import { join, basename, dirname } from "node:path";
|
import { join, basename, dirname } from "node:path";
|
||||||
import { getArchiveFileName } from "../transfer";
|
import { getArchiveFileName } from "../transfer";
|
||||||
|
import { ensureSearchablePdf, cleanupOcrTempFile } from "../helpers/pdfOcr";
|
||||||
import type { ExecFileFn } from "./types";
|
import type { ExecFileFn } from "./types";
|
||||||
|
|
||||||
// 翻譯服務優先順序(自動 fallback)
|
// 翻譯服務優先順序(自動 fallback)
|
||||||
|
|
@ -339,7 +340,19 @@ export async function convert(
|
||||||
_options?: unknown,
|
_options?: unknown,
|
||||||
execFile: ExecFileFn = execFileOriginal,
|
execFile: ExecFileFn = execFileOriginal,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
let ocrTempFile: string | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 0. 自動偵測掃描版 PDF 並進行 OCR 處理
|
||||||
|
console.log(`[PDFMathTranslate] Checking if PDF needs OCR...`);
|
||||||
|
const ocrResult = await ensureSearchablePdf(filePath, execFile);
|
||||||
|
const inputPdf = ocrResult.path;
|
||||||
|
ocrTempFile = ocrResult.tempFile;
|
||||||
|
|
||||||
|
if (ocrResult.wasOcred) {
|
||||||
|
console.log(`[PDFMathTranslate] ✅ Scanned PDF detected and OCR'd automatically`);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
|
// 1. 檢查模型(警告但不阻止,因為 pdf2zh 可能會自動下載)
|
||||||
checkModelsExist();
|
checkModelsExist();
|
||||||
|
|
||||||
|
|
@ -361,8 +374,8 @@ export async function convert(
|
||||||
const archiveDir = join(tempDir, "archive");
|
const archiveDir = join(tempDir, "archive");
|
||||||
mkdirSync(archiveDir, { recursive: true });
|
mkdirSync(archiveDir, { recursive: true });
|
||||||
|
|
||||||
// 5. 執行 pdf2zh 翻譯
|
// 5. 執行 pdf2zh 翻譯(使用 OCR 處理後的 PDF)
|
||||||
const { monoPath, dualPath } = await runPdf2zh(filePath, tempDir, targetLang, execFile);
|
const { monoPath, dualPath } = await runPdf2zh(inputPdf, tempDir, targetLang, execFile);
|
||||||
|
|
||||||
// 6. 複製翻譯後的檔案到封裝目錄
|
// 6. 複製翻譯後的檔案到封裝目錄
|
||||||
// PDFMathTranslate 輸出:
|
// PDFMathTranslate 輸出:
|
||||||
|
|
@ -440,8 +453,13 @@ export async function convert(
|
||||||
// 9. 清理臨時目錄
|
// 9. 清理臨時目錄
|
||||||
removeDir(tempDir);
|
removeDir(tempDir);
|
||||||
|
|
||||||
|
// 10. 清理 OCR 暫存檔案
|
||||||
|
cleanupOcrTempFile(ocrTempFile);
|
||||||
|
|
||||||
return "Done";
|
return "Done";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 確保清理 OCR 暫存檔案
|
||||||
|
cleanupOcrTempFile(ocrTempFile);
|
||||||
throw new Error(`PDFMathTranslate error: ${error}`);
|
throw new Error(`PDFMathTranslate error: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
src/helpers/pdfOcr.ts
Normal file
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[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
// 返回超過 100 個字元,表示 PDF 不需要 OCR
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
babeldocCalled = true;
|
babeldocCalled = true;
|
||||||
babeldocArgs = args;
|
babeldocArgs = args;
|
||||||
|
|
||||||
|
|
@ -131,7 +134,9 @@ describe("BabelDOC converter - English translation", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
babeldocArgs = args;
|
babeldocArgs = args;
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
|
@ -181,7 +186,9 @@ describe("BabelDOC converter - Traditional Chinese", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
babeldocArgs = args;
|
babeldocArgs = args;
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
|
@ -231,7 +238,9 @@ describe("BabelDOC converter - Output structure", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
const outputPath = args[outputIndex + 1];
|
const outputPath = args[outputIndex + 1];
|
||||||
|
|
@ -267,7 +276,9 @@ describe("BabelDOC converter - Output structure", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
const outputPath = args[outputIndex + 1];
|
const outputPath = args[outputIndex + 1];
|
||||||
|
|
@ -317,7 +328,9 @@ describe("BabelDOC converter - Error handling", () => {
|
||||||
_args: string[],
|
_args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
const error = new Error("Translation failed") as ExecFileException;
|
const error = new Error("Translation failed") as ExecFileException;
|
||||||
error.code = 1;
|
error.code = 1;
|
||||||
callback(error, "", "babeldoc: Translation error");
|
callback(error, "", "babeldoc: Translation error");
|
||||||
|
|
@ -337,7 +350,9 @@ describe("BabelDOC converter - Error handling", () => {
|
||||||
_args: string[],
|
_args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
// Don't create output file, simulating failed translation
|
// Don't create output file, simulating failed translation
|
||||||
callback(null, "Complete", "");
|
callback(null, "Complete", "");
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +391,9 @@ describe("BabelDOC converter - Markdown output format", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
babeldocArgs = args;
|
babeldocArgs = args;
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
|
@ -428,7 +445,9 @@ describe("BabelDOC converter - HTML output format", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "babeldoc") {
|
if (cmd === "pdftotext") {
|
||||||
|
callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", "");
|
||||||
|
} else if (cmd === "babeldoc") {
|
||||||
babeldocArgs = args;
|
babeldocArgs = args;
|
||||||
const outputIndex = args.indexOf("-o");
|
const outputIndex = args.indexOf("-o");
|
||||||
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ describe("PDFMathTranslate converter - Chinese translation", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
pdf2zhCalled = true;
|
pdf2zhCalled = true;
|
||||||
pdf2zhArgs = args;
|
pdf2zhArgs = args;
|
||||||
|
|
||||||
|
|
@ -116,7 +116,7 @@ describe("PDFMathTranslate converter - English translation", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
pdf2zhArgs = args;
|
pdf2zhArgs = args;
|
||||||
const outputDirIndex = args.indexOf("-o");
|
const outputDirIndex = args.indexOf("-o");
|
||||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||||
|
|
@ -165,7 +165,7 @@ describe("PDFMathTranslate converter - Japanese translation", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
pdf2zhArgs = args;
|
pdf2zhArgs = args;
|
||||||
const outputDirIndex = args.indexOf("-o");
|
const outputDirIndex = args.indexOf("-o");
|
||||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||||
|
|
@ -214,7 +214,7 @@ describe("PDFMathTranslate converter - Traditional Chinese", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
pdf2zhArgs = args;
|
pdf2zhArgs = args;
|
||||||
const outputDirIndex = args.indexOf("-o");
|
const outputDirIndex = args.indexOf("-o");
|
||||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||||
|
|
@ -264,7 +264,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
const outputDirIndex = args.indexOf("-o");
|
const outputDirIndex = args.indexOf("-o");
|
||||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||||
const outputDir = args[outputDirIndex + 1];
|
const outputDir = args[outputDirIndex + 1];
|
||||||
|
|
@ -308,7 +308,7 @@ describe("PDFMathTranslate converter - Output structure", () => {
|
||||||
args: string[],
|
args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
const outputDirIndex = args.indexOf("-o");
|
const outputDirIndex = args.indexOf("-o");
|
||||||
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
if (outputDirIndex !== -1 && args[outputDirIndex + 1]) {
|
||||||
const outputDir = args[outputDirIndex + 1];
|
const outputDir = args[outputDirIndex + 1];
|
||||||
|
|
@ -362,7 +362,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
|
||||||
_args: string[],
|
_args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
const error = new Error("Translation failed") as ExecFileException;
|
const error = new Error("Translation failed") as ExecFileException;
|
||||||
callback(error, "", "Error: Translation service unavailable");
|
callback(error, "", "Error: Translation service unavailable");
|
||||||
}
|
}
|
||||||
|
|
@ -381,7 +381,7 @@ describe("PDFMathTranslate converter - Error handling", () => {
|
||||||
_args: string[],
|
_args: string[],
|
||||||
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (cmd === "pdf2zh") {
|
if (cmd === "pdftotext") { callback(null, "This is a comprehensive test PDF with more than enough text content to ensure that the OCR detection threshold of 100 characters is exceeded and the PDF is not treated as a scanned document requiring OCR processing.", ""); } else if (cmd === "pdf2zh") {
|
||||||
// Don't create any output files
|
// Don't create any output files
|
||||||
callback(null, "Complete", "");
|
callback(null, "Complete", "");
|
||||||
} else if (cmd === "tar") {
|
} else if (cmd === "tar") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue