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

حزمة TypeScript SDK لواجهة PDF: خمس دقائق حتى أول استدعاء، وما تتركه لك

استخدم @pdf123/sdk من TypeScript لدمج الملفات وإضافة العلامات المائية وقراءة معلومات الملف وربط عدة أدوات في طلب واحد. تلتقط الحزمة الخطأ في اسم الأداة أو المعامل قبل رفع أي شيء، وتحمل رفوض الخادم سبباً وتلميحاً، أما إعادة المحاولة والإلغاء والإخفاقات الجزئية في الدفعات فتبقى عليك.

PDF123 · Updated 2026-09-28

عند استدعاء واجهة PDF من Node، تلتقط حزمة تطوير البرمجيات @pdf123/sdk (SDK) الخطأ في اسم الأداة أو المعامل قبل رفع أي ملف. وهي لا تعيد المحاولة، ولا تلغي نيابةً عنك، ولا تبث النتائج تدفقاً. يشير new Pdf123Client() افتراضياً وبصورة مجهولة إلى https://pdf123.xyz؛ وتُرفع ملفاتك إلى هناك وتُعالَج هناك، إذ لا يوجد محرك محلي.

مخطط: يمر الاستدعاء الواحد بثلاث بوابات بالتتابع، هي فحص tsc عند الترجمة، والتحقق وقت التشغيل قبل إرسال الطلب، ورد 400 الذي يعيده الخادم بعد الرفع؛ أما إعادة المحاولة والإلغاء فيُتركان للمستدعي، وتُرفع المدخلات التي يتجاوز مجموعها 95 MiB على أجزاء تلقائياً

يكفي ملفا PDF في مجلد لدمجهما

تحتاج إلى Node بالإصدار 20.3 أو أحدث، أو Bun. الحزمة وحدة ES: ضع الشيفرة في ملف .mjs، أو نفّذ npm pkg set type=module أولاً. ضع a.pdf وb.pdf في المجلد الحالي.

npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const merged = await client.run("merge", {
  input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));

بعد node merge.mjs تطبع الطرفية مسار الإخراج الذي مررته، أي merged.pdf، ويكون الملف في المجلد الحالي. توجد readFileInput وsaveResult في @pdf123/sdk/node. أما المدخل الجذري فيعتمد على fetch وحده ويقبل المدخلات كائنات عادية { name, data }، فيمكن لبيئة بلا نظام ملفات أن تستخدم العميل نفسه. والنتيجة هي { data, contentType, filename, json }؛ وdata مصفوفة Uint8Array كاملة، ويعطيك Buffer.from(result.data) كائن Buffer.

أداة دمج PDF ليست إلا واحدة من الأدوات.

الملفات تعود في data والتقارير في json

كل أداة تُستدعى بالصيغة client.run(toolName, { input, params }). تكمل الشيفرة أدناه merge.mjs من القسم السابق، وclient وsaveResult معرّفان فيها أصلاً. وموضع قراءة النتيجة يحدده الحقل returns الذي تعيده getTool، وقيمته إما "file" أو "json": استخدم data للأولى وjson للثانية.

import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

// client comes from merge.mjs in the previous section
const marked = await client.run("watermark", {
  input: await readFileInput("a.pdf"),
  params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));

const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"

تكتب الكتلة الأولى الملف المختوم بالعلامة المائية في marked.pdf؛ وفي الثانية يكون info.json هو التقرير. لا حاجة إلى حفظ أسماء الحقول أو قيمها الافتراضية أو القيم المسموح بها: فـ getTool("watermark")?.fields هو ذلك الكتالوج، ويقرأ الأمر pdfx describe watermark في سطر الأوامر الكتالوج نفسه. وتضم TOOLS جميع الأدوات الـ 95. والمرجع الكامل في دليل SDK في صفحة المطورين.

لتنفيذ الدمج ثم العلامة المائية ثم الضغط، يمكنك استخدام pipeline أو ثلاثة استدعاءات run متتالية؛ والاختيار بينهما يتوقف على رغبتك في الاحتفاظ بالملفات الوسيطة. وهذا أيضاً يكمل من client أعلاه. تطوي pipeline الخطوات الثلاث في طلب واحد، وتبقى النتائج الوسيطة على الخادم، ولا يتلقى المستدعي إلا الخطوة الأخيرة، وتكتبها الشيفرة أدناه في out.pdf. وإن أردت ملف كل خطوة فاستدعها منفصلة.

const result = await client.pipeline(
  [{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
  [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));

في المسار المتسلسل ثماني خطوات على الأكثر. والخطوة التاسعة تعيد HTTP 400: at most 8 pipeline steps allowed.

مجهول، أو بمفتاح، أو موجَّه إلى خادمك

إن ضبطت PDFX_API_BASE وما زال الاستدعاء يصل إلى https://pdf123.xyz، فالسبب أن new Pdf123Client() لا يقرأ متغيرات البيئة؛ إنه ينظر في خيارات المنشئ فقط.

للاستدعاءات المجهولة استخدم المنشئ كما هو. ومع مفتاح اكتب new Pdf123Client({ apiKey: process.env.PDFX_API_KEY })؛ ورأس الطلب هو X-API-KEY.

للتوجيه إلى خادمك استخدم clientFromEnv() من @pdf123/sdk/node، فهي تقرأ PDFX_API_BASE وPDFX_API_KEY. ويمكنك أيضاً كتابة new Pdf123Client({ baseUrl: "http://localhost:8080" }) مباشرة. وإن تعذّر الوصول إلى العنوان فشل الاستدعاء؛ فلا يوجد وضع غير متصل. أما ما تكسبه وما تدفعه حين تبقى الملفات داخل شبكتك فتجده في ما تكسبه فعلاً من الاستضافة الذاتية وما تكلفك.

المعامل الخاطئ يُلتقط قبل الرفع

من دون ذلك سيُرفع الملف أولاً، ولن يظهر الخطأ في اسم الحقل إلا ردّ 400 من الخادم. تُولَّد أنواع معاملات كل أداة من الكتالوج، فتفشل ثلاث زلات شائعة عند مرحلة tsc:

ما تكتبه خطأ المترجم (مقتطف)
client.run("rotat", { input }) Argument of type '"rotat"' is not assignable to parameter of type 'ToolId'
params: { watermarkTxt: "DRAFT" } Object literal may only specify known properties, but 'watermarkTxt' does not exist، وينتهي بـ Did you mean to write 'watermarkText'?
params: { angle: 45 } (التدوير يقبل 90 و180 و270 فقط) Type '45' is not assignable to type …، تليه القيم المسموح بها

الاستدعاءات الصحيحة مثل { angle: 90 } أو { watermarkText: "DRAFT", fontSize: 30 } تُترجم بلا مشكلة. والحقول الرقمية تقبل الأرقام والنصوص معاً، فـ fontSize: 30 وfontSize: "30" متكافئان. ويحصل المشروع الذي لا يستخدم TypeScript على الأخطاء نفسها وقت التشغيل، ولا يُرسل الطلب أبداً: فالحالة الثالثة تعطي Field "angle" of "rotate" must be one of: 90, 180, 270، واسم الأداة الخاطئ يعطي unknown_tool مع سرد الأسماء المشابهة في الرسالة.

حين يرفض الخادم، فرّع على reason

حين يرفض الخادم طلباً، ترمي الحزمة Pdf123Error ومعها status وcode وreason وproblem.hint. وقد يكون للقيمة code الواحدة عدة قيم reason، فافحص reason أولاً ثم ارجع إلى code. هذه ثلاثة إخفاقات شائعة، نُفِّذت محلياً بالمدخل نفسه:

المدخل status code reason hint (الأصل بالإنجليزية)
ملف PDF مشفّر دون كلمة مرور 400 bad_request password_required Provide the document password, or unlock the PDF first
كلمة مرور خاطئة 400 bad_request wrong_password Check the password and try again
ليس PDF، أو ملف تالف 400 invalid_document invalid_pdf Upload a valid, undamaged PDF; you can try repairing it first
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
  await client.run("compress", { input });
} catch (error) {
  if (!(error instanceof Pdf123Error)) throw error;
  console.error(error.status, error.code, error.reason, error.problem?.hint);
  if (error.reason === "password_required") {
    await client.run("compress", { input, password: "secret" });
  }
}

حين تعرف كلمة المرور، مرّر password لملف واحد؛ فيجري فك القفل والأداة المطلوبة في الطلب نفسه. وللملف التالف جرّب إصلاح PDF أولاً.

يجب التمييز بين صور «لا نتيجة». حين لا تجد PDF إلى CSV جدولاً في ملف PDF، يعيد الخادم 204 وترمي الحزمة code: "no_content"؛ والسبب في تصدير جدول فارغ (204): ملف PDF غالباً بلا أعمدة. أما أدوات التصفية الشرطية فتعدّ الشرط الخاطئ نتيجة عادية: لا ترمي خطأ، وتعيد matched: false مع data فارغة.

إعادة المحاولة والذاكرة والإلغاء مسؤوليتك

أخطاء الشبكة واستجابات 5xx تُرمى كما هي؛ والحزمة لا تعيد المحاولة من تلقاء نفسها. وللطلب الذي يغيّر الحالة مرّر idempotencyKey خاصاً بك: إعادة الإرسال بالمفتاح نفسه تعيد النتيجة الأولى بدل المعالجة من جديد، وتجد الآلية وحدودها في Idempotency-Key: إعادات محاولة آمنة لمهام PDF. وتُقرأ النتائج في الذاكرة كاملة، دون بث متدفق. فإذا كان المدخل والمخرج كبيرين معاً، فاحسب الذاكرة التي يشغلانها في الوقت نفسه.

الإلغاء والمهلة قيمتان مختلفتان للحقل code. والاستدعاء الواحد لا يُبلغ إلا عن الحالة التي تقع أولاً. يختبرهما المثال أدناه في استدعاءين منفصلين، حتى يمكن تشغيل الفرعين فعلاً:

import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("a.pdf");

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
  await client.run("compress", { input, signal: controller.signal });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "cancelled") { /* أنت من ألغى */ }
}

try {
  await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "timeout") { /* تجاوز timeoutMs؛ ولا signal هذه المرة */ }
}

بمجرد إطلاق إشارة الإيقاف (AbortSignal) يُرفض الطلب بالرمز code: "cancelled"؛ وتجاوز المهلة يعطي code: "timeout". لكل طلب مهلة افتراضية مدتها 5 دقائق، ويمكنك تغييرها عند إنشاء العميل أو تجاوزها في كل استدعاء بـ timeoutMs. وفي الرفع على أجزاء يحمل كل طلب هذه المهلة على حدة. والإلغاء يغلق هذا الاتصال فقط؛ ولا ضمان بأن الخادم يوقف المعالجة.

يفشل ملف وتستمر البقية، وتصل الاستدعاءات بترتيب الانتهاء

لدفعة مدخلات تُعطى لأداة ملف واحد، استخدم runBatch. إن فشل ملف واصلت الملفات الأخرى. تُستدعى onResult لحظة انتهاء كل ملف، فتعمل بترتيب الانتهاء، وindex هو موضع الملف في مصفوفة المدخلات؛ أما المصفوفة المعادة فبترتيب المدخلات دائماً. اكتب البايتات على القرص داخل هذا الاستدعاء: فـ retainData: false تفرّغ data بعد عودة الاستدعاء، فالنتيجة التي لا تحفظها هنا بـ saveResult تضيع. وإن انقطع التشغيل بقيت الملفات المكتوبة أصلاً.

افترض أن في المجلد ثلاثة ملفات PDF سليمة هي a.pdf وb.pdf وlocked.pdf المشفّر (كلمة مروره secret)، إضافة إلى ملف تالف broken.pdf لا يحوي سوى بضعة أحرف مطبوعة:

import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const results = await client.runBatch("compress", [
  fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
  concurrency: 2,
  passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
  onResult: async (entry, index) => {
    console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
    if (entry.ok) await saveResult(entry.result, { output: "out/" });
  },
  retainData: false,
});

تُعالَج افتراضياً ملفان في وقت واحد؛ وغيّر ذلك بـ concurrency. وfileSource(path) لا تقرأ الملف من القرص إلا عند حلول دوره، فلا تكون دفعة كبيرة من الملفات الكبيرة كلها في الذاكرة دفعة واحدة. وتتيح passwordFor تشغيل دفعة تخلط ملفات مقفلة وغير مقفلة مرة واحدة. وعند تشغيل الملفات الأربعة أعلاه محلياً تلقّى الاستدعاء:

1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok

قد يختلف ترتيب الانتهاء في كل تشغيل؛ وهذا ما بدا عليه تشغيل واحد فقط. وتجعل retainData: false الحقل data في المصفوفة المعادة فارغاً؛ أما الملفات الناجحة فقد كُتبت أصلاً في out/ بواسطة saveResult أعلاه. وحين تمرر idempotencyKey إلى runBatch، يكون المفتاح المرسل فعلاً لكل ملف هو <key>:<index>.

الرفع على أجزاء لا يبدأ إلا فوق 95 MiB مجتمعة

جسم الطلب المباشر محدود بـ 100 MiB، وما زاد عنه يُرفض؛ وللتفاصيل انظر حين يُرفَض رفع ملف PDF كبير: حدّ جسم الطلب البالغ 100 MiB والخطأ الذي يضلّل مسار التشخيص. وحين يتجاوز مجموع الملفات في طلب واحد 95 MiB، تنتقل الحزمة إلى الرفع على أجزاء، ولا يتغير استدعاء client.run عندك. والحد الأقصى الافتراضي للخادم لرفع واحد هو 500 MiB؛ وعند الاستضافة الذاتية اضبطه بـ PDFX_UPLOAD_MAX_BYTES.

لمقارنة العملية نفسها مكتوبة بـ curl وMCP وسطر الأوامر، انظر العملية نفسها بأربعة عملاء: المتصفح وcurl وMCP وpdfx: فذلك المقال يضع الاستدعاءات الأربعة جنباً إلى جنب، وهذا لا يفصّل إلا SDK. وصفحة الحزمة على npm هي @pdf123/sdk.

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