How-to2026-09-299 دقيقة قراءة

استخدام pdfx في الطرفية وفي CI: من أول أمر إلى سكربت موثوق

استخدم pdfx في سطر الأوامر لدمج الملفات وضغطها ووضع العلامات المائية عليها، ومعالجة ملفات كثيرة دفعة واحدة، وربط الأدوات في طلب واحد عبر pipeline. وللسكربتات وCI تعرّف على ما يُعتمد عليه: رموز الخروج، وتقرير --json، والدخل والخرج القياسيان، وسبب بقاء أداة التصفية الشرطية التي لا تطابق شيئاً على رمز الخروج 0.

PDF123 · Updated 2026-09-29

قبل أن تضع pdfx في سكربت أو في التكامل المستمر (CI)، تذكّر أمرين. رمز الخروج 0 لا يعني دائماً أن ملفاً كُتب: فأداة التصفية الشرطية التي لا تطابق شيئاً تخرج هي أيضاً بالرمز 0. و--json له شكل مختلف عند مدخل واحد عنه عند عدة مدخلات، فالسكربت الذي يحلل المصفوفة files وحدها لا يجد شيئاً إذا طابق ملف واحد فقط. وpdfx عميل HTTP: تُرفع الملفات إلى الخادم لمعالجتها، ولا يوجد محرك محلي، ولا وضع غير متصل. والمرجع الكامل للأوامر في دليل CLI في صفحة المطورين.

مخطط: يسلّم استدعاء pdfx الواحد للسكربت ثلاثة أشياء، تقرير JSON على الخرج القياسي، ورسائل الإخفاق على خرج الأخطاء القياسي، ورمز خروج؛ ويقرأ السكربت رمز الخروج أولاً

ثبّته ثم ادمج ملفين

تحتاج إلى Node بالإصدار 20.3 أو أحدث، أو Bun:

npm install -g @pdf123/cli
pdfx --version

إن فضّلت ألا تثبّته عاماً، فضع 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 فور انتهائه، دون أن ينتظر النهاية. وإن فشل ملف واصلت الملفات الأخرى، ويكون رمز الخروج في ختام الدفعة 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 في منتصف دفعة، بقيت الملفات المكتوبة أصلاً وكان رمز الخروج 130.

افتح الملف المشفّر بـ --input-password PASSWORD. وحين تخلط الدفعة ملفات مقفلة وغير مقفلة، استخدم --password-for FILE=PASSWORD، ويمكن تكراره.

استدعِ الأدوات منفصلة للملفات الوسيطة، وإلا فاستخدم pipeline

لإجراء دمج PDF ثم العلامة المائية ثم ضغط PDF، وأنت لا تحتاج إلى النتائج الوسيطة على القرص، اطوِها في طلب واحد بـ 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

في المسار المتسلسل ثماني خطوات على الأكثر؛ والخطوة التاسعة تعيد HTTP 400: at most 8 pipeline steps allowed. والأداة التي تحتاج ملفاً ثانياً (مثل overlay-pdfs التي تراكب ملف PDF آخر) لا تصلح خطوة في مسار متسلسل. يرفضها pdfx قبل الرفع، برمز الخروج 2 والرسالة Tool "overlay-pdfs" needs a second file and cannot run as a pipeline step.

لا تستخدم الدخل القياسي إلا حين تريد وصل الأمر بأنبوب. الوسيط - يقرأ من الدخل القياسي، و-o - يكتب النتيجة إلى الخرج القياسي:

cat report.pdf | pdfx compress - -o - > report-small.pdf

في هذا الوضع لا يحمل الخرج القياسي سوى بايتات الملف؛ أما الرسائل والأخطاء فتذهب إلى خرج الأخطاء القياسي، فإعادة التوجيه آمنة. جرّبنا ذلك بملف PDF حجمه نحو 1 KB: حمل الخرج القياسي ملف PDF حجمه 1040 بايت، وهو الحجم نفسه للملف المكتوب بـ -o، وكان خرج الأخطاء القياسي فارغاً. وحين تقرأ من الدخل القياسي ولا تعطي -o، تُسمّى النتيجة stdin.pdf، وتُكتب في المجلد الحالي مع ملاحظة على خرج الأخطاء القياسي.

يجب أن يطابق السكربت السبب لا الرسالة

رمز الخروج المعنى أمثلة
0 نجاح، أو أداة تصفية شرطية لم تطابق شيئاً نجح الدمج؛ وكان شرط filter-page-count خاطئاً
1 أُرسل الطلب لكنه فشل؛ وفي الدفعة فشل ملف واحد على الأقل كلمة مرور خاطئة، ليس PDF، انتهت المهلة، لا شيء يُعاد
2 خطأ في الاستخدام، ولم يُرفع شيء اسم أداة خاطئ، خطوة في مسار متسلسل تحتاج ملفاً ثانياً، نوع نتيجة يناقض امتداد ملف الإخراج
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 ثانية.

حين يكون الشرط خاطئاً يبقى رمز الخروج 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 }

لإبقاء الملفات التي تزيد على صفحتين فقط، يمكنك كتابتها هكذا. شغّلناها محلياً على a.pdf (صفحة واحدة) وthree.pdf (ثلاث صفحات)، فبقي الأخير وحده:

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 لا تقرر إلا هل تُطبع عبارة التخطي، لا هل يُكتب الملف.

مع مدخل واحد فقط لا توجد مصفوفة files في --json

مع عدة مدخلات يكون الخرج القياسي تحت --json تقريراً كاملاً. input مسار مطلق. وprocessed يعدّ الملفات التي نجح طلبها، ومنها التي لا مطابقة فيها. وunmatched عدد الملفات من هذه التي لا مطابقة فيها، وسجلاتها { "ok": true, "matched": false } بلا path. وحين لا يطابق أي ملف في الدفعة يبقى رمز الخروج 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 كما في قسم التصفية أعلاه.

إن رُفض أي ملف فشلت هذه الخطوة وسردت في السجل أسماء الملفات وأسبابها. المنطق هو نفسه: اقرأ رمز الخروج أولاً، واطبع خرج الأخطاء القياسي عند الإخفاق، ثم استخدم 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"

شغّلناه محلياً بأربعة أنواع من المدخلات:

الملفات في المجلد رمز الخروج السجل
ملفا PDF سليمان 0 لا شيء
ملفا PDF سليمان وملف تالف 1 pdfx: broken.pdf: HTTP 400: ...، ثم سطر broken.pdf invalid_pdf
ملف PDF سليم واحد 0 لا شيء؛ وreport.json بصيغة الملف الواحد
ملف PDF تالف واحد 1 reason: invalid_pdf وcode: invalid_document وسطر hint؛ وreport.json فارغ

علامة الاستفهام في آخر .files[]? في jq تمنع تقرير الملف الواحد (الذي لا يحوي files) من إثارة خطأ. والملف التالف الوحيد لا تقرير دفعة له، ولا يظهر السبب إلا في errors.log، فاحتفظ بسطري الخرج كليهما.

وثمة ثلاثة أمور أخرى عليك التحقق منها قبل أن تضع هذا في CI. يحتاج pdfx إلى الشبكة: تُرفع الملفات إلى PDFX_API_BASE وقيمته الافتراضية https://pdf123.xyz. وللمستندات التي يجب أن تبقى داخل شبكتك، شغّل خدمة بنفسك ووجّه هذا المتغير إليها؛ انظر ما تكسبه فعلاً من الاستضافة الذاتية وما تكلفك. وحين تحتاج إلى هوية، ضع المفتاح في PDFX_API_KEY لا في وسائط سطر الأوامر، حيث يبقى في قائمة العمليات وفي السجلات. والإصدار المنشور حالياً هو 0.1.0؛ وفي CI استخدم npx @pdf123/[email protected] ... لتثبيته، حتى لا يتغير تنسيق الخرج ورموز الخروج مع الإصدارات الجديدة، وتصير الترقية تغييراً تجريه عن قصد.

صفحة الحزمة على npm هي @pdf123/cli.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool