ใช้ pdfx ในเทอร์มินัลและ CI: จากคำสั่งแรกสู่สคริปต์ที่ไว้ใจได้
ใช้ pdfx บนบรรทัดคำสั่งเพื่อรวม บีบอัด และใส่ลายน้ำ ประมวลผลหลายไฟล์พร้อมกัน และต่อเครื่องมือเป็นคำขอเดียวด้วย pipeline สำหรับสคริปต์และ CI ให้รู้ว่าอะไรควรพึ่งพา ได้แก่ exit code รายงาน --json อินพุตและเอาต์พุตมาตรฐาน และเหตุผลที่เครื่องมือกรองตามเงื่อนไขที่ไม่พบรายการตรงก็ยังจบด้วย 0

ก่อนนำ pdfx ไปใส่ในสคริปต์หรือการทำ continuous integration (CI) ให้จำสองข้อ ข้อแรก exit code 0 ไม่ได้แปลว่ามีไฟล์ถูกเขียนเสมอไป เครื่องมือกรองตามเงื่อนไขที่ไม่พบรายการตรงก็จบด้วย 0 เช่นกัน ข้อสอง --json มีรูปแบบต่างกันระหว่างอินพุตเดียวกับหลายอินพุต สคริปต์ที่อ่านแค่อาร์เรย์ files จะไม่พบอะไรเลยเมื่อมีไฟล์ตรงเงื่อนไขเพียงไฟล์เดียว pdfx เป็นไคลเอนต์ HTTP ไฟล์ถูกอัปโหลดไปประมวลผลบนเซิร์ฟเวอร์ ไม่มี engine ในเครื่อง และไม่มีโหมดออฟไลน์ เอกสารอ้างอิงคำสั่งฉบับเต็มอยู่ในคู่มือ CLI บนหน้านักพัฒนา
ติดตั้ง แล้วรวมสองไฟล์
ต้องใช้ Node 20.3 ขึ้นไป หรือ Bun:
npm install -g @pdf123/cli
pdfx --version
ถ้าไม่อยากติดตั้งแบบ global ให้ใส่ npx @pdf123/cli นำหน้าคำสั่ง เมื่อมี PDF สองไฟล์ในไดเรกทอรีปัจจุบัน:
pdfx merge a.pdf b.pdf -o merged.pdf
เมื่อสำเร็จ เอาต์พุตมาตรฐานจะพิมพ์ merged.pdf นั่นคือ รวม PDF ก่อนจะเปลี่ยนไปใช้เครื่องมืออื่น ให้ขอดูฟิลด์แทนการเดาชื่อพารามิเตอร์:
pdfx describe watermark
Add Watermark (watermark) - Add text or image watermarks to PDF files
Input: 1 file (.pdf)
Result: file
Fields:
--watermarkText <value> Watermark text [default: PDF123]
--fontSize <number> Font size [default: 30]
min 6, max 200
(ตัดตอนมา ฟิลด์ยังมี --rotation, --customColor และอื่น ๆ) ฟิลด์ก็คือออปชันบรรทัดคำสั่งธรรมดา:
pdfx watermark in.pdf --watermarkText DRAFT -o marked.pdf
pdfx list แสดงเครื่องมือทั้ง 95 ตัว --category security แสดงเฉพาะหมวดเดียว และ --query watermark ค้นหาด้วยคำ
หลายไฟล์ไปลงไดเรกทอรี และไฟล์ที่มีอยู่จะไม่ถูกเขียนทับ
ส่งหลายอินพุตให้เครื่องมือที่รับไฟล์เดียว แต่ละไฟล์จะถูกเขียนลงไดเรกทอรีที่ระบุด้วย -o ทันทีที่เสร็จ ไม่รอจนจบทั้งชุด ถ้าไฟล์หนึ่งล้มเหลว ไฟล์อื่นยังทำต่อ และ exit code ตอนจบงานชุดคือ 1
pdfx compress a.pdf b.pdf -o small/
เอาต์พุตมาตรฐานของการรันหนึ่งครั้งเป็นดังนี้ และลำดับอาจต่างกันทุกครั้ง:
a.pdf -> small/a.pdf
b.pdf -> small/b.pdf
ไดเรกทอรีที่มีอยู่แล้วใช้ได้เลย ถ้าไดเรกทอรียังไม่มี -o ต้องลงท้ายด้วย / ไม่เช่นนั้น small จะถูกมองเป็นชื่อไฟล์ ถ้าไม่ใส่ -o ผลลัพธ์จะลงไดเรกทอรีปัจจุบันตามชื่อที่เซิร์ฟเวอร์ตั้งให้ ไฟล์ที่มีอยู่จะไม่ถูกเขียนทับ และไฟล์ใหม่จะเป็น a-1.pdf, a-2.pdf ส่วนชื่อไฟล์ที่ระบุเฉพาะ (-o same.pdf) จะแทนที่เนื้อหาเดิมตามที่คุณสั่ง โดยค่าเริ่มต้นจะประมวลผลครั้งละสองไฟล์ ปรับได้ด้วย --concurrency ถ้ากด Ctrl-C กลางงานชุด ไฟล์ที่เขียนไปแล้วยังอยู่ และ exit code คือ 130
เปิดไฟล์ที่เข้ารหัสด้วย --input-password PASSWORD เมื่องานชุดผสมไฟล์ที่ล็อกและไม่ล็อก ใช้ --password-for FILE=PASSWORD ซึ่งใส่ซ้ำได้
เรียกเครื่องมือแยกกันถ้าต้องการไฟล์ระหว่างทาง ไม่เช่นนั้นใช้ pipeline
ถ้าจะรวม ใส่ลายน้ำ แล้วบีบอัด และไม่ต้องการผลลัพธ์ระหว่างทางบนดิสก์ ให้พับเป็นคำขอเดียวด้วย pipeline ไฟล์ระหว่างทางจะไม่ถูกดาวน์โหลดแล้วอัปโหลดซ้ำ ถ้าต้องการไฟล์จากแต่ละขั้น ให้เรียกเครื่องมือแยกกัน pipeline ให้ไฟล์เดียว
pdfx pipeline a.pdf b.pdf --step merge --step compress -o merged-small.pdf
# When a step needs parameters, describe the steps in JSON
pdfx pipeline a.pdf b.pdf \
--steps '[{"tool":"merge"},{"tool":"watermark","params":{"watermarkText":"DRAFT"}},{"tool":"compress"}]' \
-o out.pdf
pipeline มีได้อย่างมาก 8 ขั้นตอน ขั้นที่ 9 จะได้ HTTP 400: at most 8 pipeline steps allowed เครื่องมือที่ต้องใช้ไฟล์ที่สอง (เช่น overlay-pdfs ที่ซ้อน PDF อีกไฟล์ทับ) ใช้เป็นขั้นของ pipeline ไม่ได้ pdfx จะปฏิเสธก่อนอัปโหลด ด้วย exit code 2 และข้อความ Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step
ใช้อินพุตมาตรฐานเฉพาะเมื่อต้องการต่อคำสั่งเข้ากับ pipe อาร์กิวเมนต์ไฟล์ - อ่านจากอินพุตมาตรฐาน และ -o - เขียนผลลัพธ์ไปยังเอาต์พุตมาตรฐาน:
cat report.pdf | pdfx compress - -o - > report-small.pdf
ในโหมดนี้ เอาต์พุตมาตรฐานมีเฉพาะไบต์ของไฟล์ ข้อความและข้อผิดพลาดไปที่ข้อผิดพลาดมาตรฐาน การเปลี่ยนเส้นทางจึงปลอดภัย เราตรวจกับ PDF ขนาดราว 1 KB แล้ว เอาต์พุตมาตรฐานเป็น PDF ขนาด 1040 ไบต์ เท่ากับไฟล์ที่เขียนด้วย -o และข้อผิดพลาดมาตรฐานว่างเปล่า เมื่ออ่านจากอินพุตมาตรฐานโดยไม่ใส่ -o ผลลัพธ์จะตั้งชื่อว่า stdin.pdf เขียนลงไดเรกทอรีปัจจุบันพร้อมหมายเหตุทางข้อผิดพลาดมาตรฐาน
สคริปต์ควรจับคู่ที่เหตุผล ไม่ใช่ข้อความ
| Exit code | ความหมาย | ตัวอย่าง |
|---|---|---|
| 0 | สำเร็จ หรือเครื่องมือกรองตามเงื่อนไขที่ไม่พบรายการตรง | รวมสำเร็จ เงื่อนไขของ filter-page-count เป็นเท็จ |
| 1 | ส่งคำขอแล้วแต่ล้มเหลว ในงานชุดคืออย่างน้อยหนึ่งไฟล์ล้มเหลว | รหัสผ่านผิด ไม่ใช่ PDF หมดเวลา ไม่มีอะไรให้คืน |
| 2 | ใช้คำสั่งผิด ไม่มีอะไรถูกอัปโหลด | ชื่อเครื่องมือสะกดผิด ขั้นของ pipeline ที่ต้องใช้ไฟล์ที่สอง ชนิดผลลัพธ์ขัดกับนามสกุลของไฟล์เอาต์พุต |
| 130 | คุณขัดจังหวะเอง | Ctrl-C ระหว่างงานชุด |
เมื่อล้มเหลว นอกจากข้อความหนึ่งบรรทัดแล้ว ข้อผิดพลาดมาตรฐานยังมีอีกสามบรรทัด คือ reason:, code: และ hint: ไฟล์ที่เข้ารหัสพร้อมรหัสผ่านผิดให้ผลนี้ในเครื่อง:
pdfx: HTTP 400: The password is incorrect.
reason: wrong_password
code: bad_request
hint: Check the password and try again.
วิธีส่งรหัสผ่านเปลี่ยนบรรทัดแรกของข้อความ unlock --password ให้บรรทัดข้างบน ส่วน --input-password กับเครื่องมืออื่น เซิร์ฟเวอร์ถือการปลดล็อกเป็นขั้นภายใน และบรรทัดแรกคือ HTTP 400: Pipeline step 0 (security/remove-password) failed: The password is incorrect. ขณะที่บรรทัด reason: เป็น wrong_password ทั้งสองกรณี ถ้าไม่ให้รหัสผ่านเลย ข้อความคือ This PDF is password-protected. Enter its password. และ reason เป็น password_required สคริปต์ควรจับคู่บรรทัด reason: ส่วน code หยาบกว่า และค่าที่พบบ่อยคือ bad_request
ชื่อเครื่องมือที่สะกดผิดจบด้วย 2 และข้อความจะเสนอชื่อที่คล้ายกัน:
pdfx: Unknown tool "compres". Did you mean: compress, decompress-pdf? Run `pdfx list` to see all tools.
ชนิดผลลัพธ์ที่ขัดกับชื่อไฟล์ก็จบด้วย 2 เช่นกัน และเกิดก่อนมีการเขียนอะไร ตัวอย่างคือเขียน ZIP ที่ แยก PDF สร้างลงใน x.pdf:
pdfx: The result is application/zip but "x.pdf" has a .pdf extension; name it *.zip or write into a directory
code: output_mismatch
การหมดเวลาก็จบด้วย 1 โดย code เป็น timeout หน่วยของ --timeout คือมิลลิวินาที และค่าเริ่มต้นคือ 300000 หรือ 5 นาที ดังนั้น --timeout 60 หมายถึง 60 มิลลิวินาที ไม่ใช่ 60 วินาที
เมื่อเงื่อนไขเป็นเท็จ exit code ก็ยังเป็น 0
เครื่องมือกลุ่มหนึ่งตอบคำถามใช่หรือไม่ใช่ จำนวนหน้ามากกว่า N หรือไม่ ไฟล์มีข้อความบางอย่างหรือไม่ ไฟล์ใหญ่กว่าขนาดที่กำหนดหรือไม่ ถ้าคำตอบคือใช่ เครื่องมือจะคืนไฟล์อินพุตโดยไม่แก้ไข ถ้าไม่ใช่ จะไม่คืนอะไร และ pdfx พิมพ์บรรทัด no match แล้วจบด้วย 0 เครื่องมือกรองเหล่านี้มีเฉพาะใน SDK บรรทัดคำสั่ง และ MCP เว็บไซต์ไม่มีหน้าสำหรับมัน
ต่างจากผลลัพธ์ว่างอีกแบบ เมื่อ PDF เป็น CSV ไม่พบตารางใน PDF เซิร์ฟเวอร์ตอบ 204 และ pdfx จบด้วย 1 พร้อมรายงาน no_content เครื่องมือนั้นมีเจตนาสร้างเนื้อหาแต่ไม่ได้สร้าง เหตุผลอยู่ใน ส่งออกตารางว่าง (204): PDF ของคุณน่าจะไม่มีคอลัมน์ สำหรับเครื่องมือกรอง "ไม่พบรายการตรง" คือคำตอบที่มันควรให้
หากต้องการอ่านในสคริปต์ ให้ใช้ --json เมื่อตรง จะพิมพ์ไฟล์ที่เขียน เมื่อไม่ตรง จะพิมพ์ { "matched": false }:
# Match: the file is written, and its info is printed
$ pdfx filter-page-count three.pdf --pageCount 2 --comparator Greater --json -o big/
{ "path": "big/three.pdf", "contentType": "application/pdf", "bytes": 2594 }
# No match: no file is written
$ pdfx filter-page-count three.pdf --pageCount 5 --comparator Greater --json
{ "matched": false }
ถ้าจะเก็บเฉพาะไฟล์ที่มีมากกว่า 2 หน้า เขียนได้ดังนี้ เรารันในเครื่องกับ a.pdf (1 หน้า) และ three.pdf (3 หน้า) และเก็บไว้เฉพาะไฟล์หลัง:
mkdir -p big
for f in *.pdf; do
if pdfx filter-page-count "$f" --pageCount 2 --comparator Greater --json -o big/ \
| jq -e '.matched == false' >/dev/null; then
echo "$f: ข้ามแล้ว"
fi
done
jq -e '.matched == false' จบด้วย 0 เมื่อไม่ตรง และ 1 เมื่อตรง เมื่อตรง pdfx เขียนไฟล์ลงใน big/ เรียบร้อยแล้ว if ตัดสินแค่ว่าจะพิมพ์ "ข้ามแล้ว" หรือไม่ ไม่ได้ตัดสินว่าจะเขียนไฟล์หรือไม่
เมื่อมีอินพุตเดียว --json จะไม่มีอาร์เรย์ files
เมื่อมีหลายอินพุต เอาต์พุตมาตรฐานภายใต้ --json เป็นรายงานเต็ม input เป็นพาธสัมบูรณ์ processed นับไฟล์ที่คำขอสำเร็จ รวมถึงไฟล์ที่ไม่พบรายการตรง unmatched คือจำนวนของไฟล์เหล่านั้นที่ไม่ตรง และรายการของมันเป็น { "ok": true, "matched": false } ไม่มี path เมื่อทุกไฟล์ในงานชุดไม่ตรง exit code ก็ยังเป็น 0 เมื่อส่ง --idempotency-key ให้งานชุด คีย์ที่ส่งจริงของแต่ละไฟล์คือ <key>:<index> กลไกอยู่ใน Idempotency-Key: การลองใหม่ที่ปลอดภัยสำหรับงาน PDF
{
"processed": 2,
"unmatched": 0,
"failed": 1,
"files": [
{ "input": "/work/a.pdf", "ok": true, "path": "out/a.pdf", "contentType": "application/pdf", "bytes": 1040 },
{ "input": "/work/broken.pdf", "ok": false, "error": "HTTP 400: The file is not a valid PDF or it is damaged.", "reason": "invalid_pdf" },
{ "input": "/work/b.pdf", "ok": true, "path": "out/b.pdf", "contentType": "application/pdf", "bytes": 1027 }
]
}
เมื่อมีอินพุตเดียว จะใช้เส้นทางไฟล์เดียว --json พิมพ์ { "path": ..., "contentType": ..., "bytes": ... } และไม่มีอาร์เรย์ files เมื่อล้มเหลว เอาต์พุตมาตรฐานว่างและทุกอย่างอยู่ที่ข้อผิดพลาดมาตรฐาน เมื่อขยายไฟล์ด้วย glob สคริปต์ต้องรองรับทั้งสองรูปแบบ ไม่ว่าจะตรงหนึ่งไฟล์หรือหลายไฟล์
รวมกฎข้างบนเป็นขั้นตอน CI เดียว
สคริปต์นี้บีบอัดอย่างเดียวและไม่ใช้เครื่องมือกรอง การบีบอัดล้มเหลวจะจบด้วย 1 และขั้นตอนก็ล้มตามไปด้วย เครื่องมือกรองที่ไม่พบรายการตรงจบด้วย 0 CI จึงไม่ล้มเพียงเพราะไม่มีรายการตรง ส่วนการไม่พบรายการตรงนับเป็นปัญหาหรือไม่ คุณเป็นผู้ตัดสินจากการอ่าน --json ดังในหัวข้อเครื่องมือกรองด้านบน
ถ้ามีไฟล์ใดถูกปฏิเสธ ขั้นตอนนี้จะล้มและแสดงชื่อไฟล์กับเหตุผลในล็อก ตรรกะเหมือนเดิม คืออ่าน exit code ก่อน พิมพ์ข้อผิดพลาดมาตรฐานเมื่อล้มเหลว แล้วใช้ jq ดึง reason จากรายงานงานชุด ถ้าไม่ส่งอาร์กิวเมนต์ไดเรกทอรี สคริปต์จะออกทันที เพื่อไม่ให้ "$1"/*.pdf ขยายเป็น /*.pdf
#!/usr/bin/env bash
# วิธีใช้: ./ci-step.sh docs
docs="${1:?วิธีใช้: ./ci-step.sh <ไดเรกทอรี>}"
mkdir -p out
pdfx compress "$docs"/*.pdf -o out/ --json > report.json 2> errors.log
status=$?
if [ "$status" -ne 0 ]; then
cat errors.log >&2
jq -r '.files[]? | select(.ok | not) | "\(.input | split("/") | last)\t\(.reason)"' report.json >&2
fi
exit "$status"
เรารันในเครื่องกับอินพุตสี่แบบ:
| ไฟล์ในไดเรกทอรี | Exit code | ล็อก |
|---|---|---|
| PDF ที่ใช้ได้สองไฟล์ | 0 | ไม่มี |
| PDF ที่ใช้ได้สองไฟล์ กับไฟล์เสียหนึ่งไฟล์ | 1 | pdfx: broken.pdf: HTTP 400: ..., then a line broken.pdf invalid_pdf |
| PDF ที่ใช้ได้ไฟล์เดียว | 0 | ไม่มี report.json อยู่ในรูปแบบไฟล์เดียว |
| PDF ที่เสียไฟล์เดียว | 1 | reason: invalid_pdf, code: invalid_document and a hint line; report.json is empty |
เครื่องหมายคำถามท้าย .files[]? ใน jq ป้องกันไม่ให้รายงานไฟล์เดียว (ซึ่งไม่มี files) ทำให้เกิดข้อผิดพลาด ไฟล์เสียไฟล์เดียวไม่มีรายงานงานชุด เหตุผลปรากฏเฉพาะใน errors.log จึงควรเก็บเอาต์พุตทั้งสองบรรทัดไว้
เมื่อนำไปใส่ใน CI ยังมีอีกสามเรื่องที่ควรตรวจสอบก่อน pdfx ต้องใช้เครือข่าย ไฟล์ถูกอัปโหลดไปที่ PDFX_API_BASE ซึ่งค่าเริ่มต้นคือ https://pdf123.xyz สำหรับเอกสารที่ต้องอยู่ในเครือข่ายของคุณ ให้รันบริการเองแล้วชี้ตัวแปรนี้ไปที่บริการนั้น ดู การโฮสต์เองให้อะไรจริง ๆ (และต้องแลกด้วยอะไร) เมื่อต้องการระบุตัวตน ให้ใส่คีย์ใน PDFX_API_KEY ไม่ใช่ในอาร์กิวเมนต์บรรทัดคำสั่ง เพราะจะค้างอยู่ในรายการโพรเซสและล็อก เวอร์ชันที่เผยแพร่ตอนนี้คือ 0.1.0 ใน CI ใช้ npx @pdf123/[email protected] ... เพื่อตรึงเวอร์ชัน ให้รูปแบบเอาต์พุตและ exit code ไม่เปลี่ยนตามรุ่นใหม่ และการอัปเกรดเป็นการเปลี่ยนที่คุณทำโดยตั้งใจ
หน้าของแพ็กเกจบน npm คือ @pdf123/cli