تخطَّ إلى المحتوى
getnextpdf.com

قسّم ملف ⁨PDF⁩ واستخرج نطاقات الصفحات

لديك ملف ⁨PDF⁩ واحد، وتحتاج إلى عدّة. تنحت هذه الوصفة مستندًا واحدًا إلى ملفات متعددة عبر واجهة التقسيم في ⁨Core⁩، وهي NextPDF\Document\PdfSplitter. تمرّر المصدر بوصفه سلسلة بايتات ⁨PDF⁩ خامًا وتصف الصفحات التي تريدها. يحلّل المُقسِّم المصدر عبر رسم الكائنات، وينسخ الكائنات القابلة للوصول من كل نطاق مطلوب إلى مستند جديد مُعاد ترقيمه له شجرة صفحاته وجدول مراجعه التقاطعية الخاصان، ويعيد إليك ملفات ⁨PDF⁩ مكتملة بنيويًا تُحمَّل في قارئ متوافق.

هذا عكس وصفة الدمج: يؤلّف الدمج مستندات كثيرة في واحد، ويفكّك التقسيم مستندًا واحدًا إلى مستندات كثيرة. وتغطّي الواجهة نفسها المهام الثلاث التي تحتاجها أكثر ما تحتاج:

  • التقسيم بالنطاقات — أنتج مستند إخراج واحدًا لكل نطاق صفحات تسمّيه.
  • التقسيم كل ⁨N⁩ صفحات — قطّع ملفًا طويلًا إلى أجزاء ثابتة الحجم.
  • استخراج نطاق — اسحب نطاق صفحات متّصلًا واحدًا إلى مستند واحد.

يجري التقسيم داخل العملية، دون متصفّح بلا واجهة أو نداء شبكي. تحتاج إلى تثبيت ⁨Core⁩ ‏(composer require nextpdf/core:^3) وملف ⁨PDF⁩ واحد قابل للقراءة.

Terminal window
composer require nextpdf/core:^3

يحدّد ملف ⁨PDF⁩ موقع صفحاته عبر شجرة صفحات جذرها عقدة /Pages، ويصل إلى كل كائن غير مباشر عبر بيانات مراجعه التقاطعية (جدول أو مجرى). لا يمكنك استخراج الصفحات بتقطيع البايتات: فالصفحة الواحدة تشير إلى خطوط وصور وقواميس موارد مشتركة تقيم في مكان آخر من الملف، ولن تبقى إزاحات المراجع التقاطعية صالحة.

يؤدي PdfSplitter العمل الفعلي. فلكل نطاق يجوب رسم الكائنات انطلاقًا من كائنات الصفحات المطلوبة، ويجمع إغلاق الكائنات القابل للوصول، ويعيد ترقيم تلك الكائنات في فضاء عناوين جديد، ويعيد بناء مستند بشجرة صفحات واحدة، ويُصدر جدول مراجع تقاطعية حقيقيًا وفق بنية ⁨PDF 2.0⁩ ‏(⁨ISO 32000-2:2020⁩، جدول المراجع التقاطعية §⁨7.5.4⁩، شجرة الصفحات §⁨7.7.3⁩). كل إخراج مستند مكتفٍ بذاته، لا قطعة منه.

أرقام الصفحات تبدأ من 1 وشاملة. والنطاق كائن قيمة من NextPDF\Document\PageRange: يعني new PageRange(2, 5) الصفحات من 2 إلى 5. يتحقق المُنشئ من ثوابته الخاصة — فهو يرفض بداية أقل من 1 أو نهاية قبل البداية برفع NextPDF\Exception\PageLayoutException — فيفشل النطاق المستحيل عند الإنشاء، لا في عمق المُقسِّم. ويرفع كل من PageRange::parse() وPageRange::all()PageLayoutException نفسه عند مواصفة مشوّهة أو إجمالي صفحات غير موجب.

يكشف new NextPDF\Document\PdfSplitter() عن ثلاث طرائق. تأخذ جميعها المصدر بوصفه سلسلة بايتات ⁨PDF⁩ خامًا، لا مسارًا أبدًا.

  • split(string $pdfData, array $ranges, int $maxBytes = 100_000_000, int $maxRanges = 1000): SplitResult ينتج مستند إخراج واحدًا لكل PageRange في $ranges، بالترتيب. ويحدّ المعاملان المحدّان حجم المدخل وعدد النطاقات.
  • splitEvery(string $pdfData, int $pagesPerSegment): SplitResult يقطّع المستند إلى أجزاء ثابتة الحجم بمقدار $pagesPerSegment صفحة لكل جزء؛ ويحمل الجزء الأخير الباقي.
  • extractPages(string $pdfData, PageRange $range): SplitDocument يستخرج نطاقًا واحدًا ويعيد ذلك المستند الواحد مباشرةً.

يعيد split() وsplitEvery() كائن NextPDF\Document\SplitResult، وهو كائن readonly يحمل $documents (قائمة بالأجزاء) و$totalPages (صفحات المصدر) و$sourceSize. ويتيح count() وdocument(int $index) لجلب جزء بفهرسه المبدوء بالصفر، وtotalOutputSize().

كل جزء، وقيمة إرجاع extractPages()، كائن NextPDF\Document\SplitDocument: كائن readonly يكشف عن $pdfData (بايتات الجزء) و$range و$pageCount و$sizeBytes والمساعد isValid(). إن isValid() فحص سلامة ضيّق لترويسة %PDF — يعيد true حين تبدأ بايتات الجزء بـ%PDF — لا تحقق من بنية المستند أو من المطابقة؛ فهو يؤكد أن المُقسِّم أنتج ملف ⁨PDF،⁩ لا أن الملف مطابق تمامًا.

تبني PageRange مباشرةً بـnew PageRange($start, $end)، أو تحلّل مواصفة مقروءة للبشر بـPageRange::parse('1-3,5,7-10') التي تعيد list<PageRange> جاهزة لتمريرها إلى split(). ويعيد PageRange::all($totalPages) نطاقًا واحدًا يغطّي المستند كله.

يقرأ هذا النموذج ملفًا واحدًا ويقسّمه إلى مستندين: الصفحات من 1 إلى 3، والصفحات من 4 إلى 6. ويترك معالجة الأخطاء جانبًا ليُظهر شكل الاستدعاء؛ ويضيف نموذج الإنتاج أدناه كامل الحراسات.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->split(
file_get_contents(__DIR__ . '/report.pdf'),
[
new PageRange(1, 3),
new PageRange(4, 6),
],
);
foreach ($result->documents as $i => $segment) {
file_put_contents(__DIR__ . sprintf('/part-%d.pdf', $i + 1), $segment->pdfData);
}
printf("Split %d-page source into %d document(s).\n", $result->totalPages, $result->count());

يبني هذا البرنامج المكتفي بذاته مستندًا صغيرًا متعدّد الصفحات في الذاكرة، فيعمل دون ملف خارجي. وهو يبرهن على العمليات الثلاث جميعها — التقسيم بالنطاقات، والتقسيم كل ⁨N⁩ صفحات، واستخراج نطاق واحد. يتحقق من أجزاء النطاقات والذيل المستخرج ويكتبها، ويبلّغ عن نتيجة التقسيم بالحجم بصورة عدد، فترى شكل كل استدعاء دون ثلاث حلقات كتابة شبه متطابقة. يلتقط الاستثناءات التي ترفعها واجهة التقسيم ويعيد رفع كلٍّ منها بسياق بدلًا من ابتلاعها. استبدل المصدر في الذاكرة بقراءتك أنت عبر file_get_contents() أو بجلب من مخزن كائنات.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use InvalidArgumentException;
use NextPDF\Core\Document;
use NextPDF\Document\Merge\UnsupportedSourceDocumentException;
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
use NextPDF\Document\SplitDocument;
use NextPDF\Exception\PageLayoutException;
/**
* Build a tiny labelled multi-page PDF so the program is self-contained.
*
* In your own code, replace this with a read of the PDF you want to split,
* for example file_get_contents($path).
*/
function buildSample(int $pages): string
{
$doc = Document::createStandalone();
$doc->setTitle('Split sample');
for ($page = 1; $page <= $pages; $page++) {
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, sprintf('Source page %d', $page), newLine: true);
}
return $doc->getPdfData();
}
$source = buildSample(7);
$splitter = new PdfSplitter();
try {
// 1. Split into named ranges: one output per PageRange, in order.
$byRange = $splitter->split(
$source,
PageRange::parse('1-3,4-6'),
maxBytes: 50_000_000,
maxRanges: 100,
);
// 2. Split every 2 pages: segments of [1-2], [3-4], [5-6], [7] (remainder).
$bySize = $splitter->splitEvery($source, 2);
// 3. Extract a single range as one document.
$tail = $splitter->extractPages($source, new PageRange(7, 7));
} catch (InvalidArgumentException $e) {
// Raised on an oversized input, an empty range list, or too many ranges.
throw new RuntimeException('Split rejected its input: ' . $e->getMessage(), previous: $e);
} catch (PageLayoutException $e) {
// Raised when a range exceeds the source page count, and also by the
// PageRange constructor / PageRange::parse() on an invalid or malformed range.
throw new RuntimeException(
sprintf('Range out of bounds (page %d): %s', $e->getPageNumber(), $e->getConstraint()),
previous: $e,
);
} catch (UnsupportedSourceDocumentException $e) {
// Raised fail-closed on an encrypted, signed, or form-bearing source.
throw new RuntimeException('Source cannot be split: ' . $e->getMessage(), previous: $e);
}
printf(
"Source has %d page(s). By-range produced %d doc(s); by-size produced %d doc(s).\n",
$byRange->totalPages,
$byRange->count(),
$bySize->count(),
);
foreach ($byRange->documents as $i => $segment) {
emitSegment(sprintf('range-%d', $i + 1), $segment);
}
emitSegment('tail', $tail);
/**
* Validate a segment and write it to the cookbook side-channel directory,
* or to the script directory by default.
*/
function emitSegment(string $name, SplitDocument $segment): void
{
if (!$segment->isValid()) {
throw new RuntimeException(sprintf('Segment "%s" failed its %%PDF header check.', $name));
}
$dir = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$dir = $dir !== false && $dir !== '' ? $dir : __DIR__;
$path = sprintf('%s/%s.pdf', rtrim($dir, '/'), $name);
if (file_put_contents($path, $segment->pdfData) === false) {
throw new RuntimeException(sprintf('Could not write segment to "%s".', $path));
}
printf("Wrote %s: pages %d-%d, %d bytes.\n", $name, $segment->range->start, $segment->range->end, $segment->sizeBytes);
}

الخرج القياسي المتوقع (تعتمد أحجام البايتات على البناء):

Source has 7 page(s). By-range produced 2 doc(s); by-size produced 4 doc(s).
Wrote range-1: pages 1-3, <n> bytes.
Wrote range-2: pages 4-6, <n> bytes.
Wrote tail: pages 7-7, <n> bytes.
  • المصدر بايتات، لا مسار. كل طريقة تأخذ سلسلة ⁨PDF⁩ خامًا. اقرأ الملف بـ file_get_contents() أولًا، أو اسحب البايتات من مخزن كائنات. يجعل تمرير مسار المصدر يفشل في التحليل.
  • أرقام الصفحات تبدأ من 1 وشاملة. يغطّي new PageRange(1, 3) الصفحات 1 و2 و3 — ثلاث صفحات. وترفع بداية أقل من 1 أو نهاية قبل البداية PageLayoutException من مُنشئ PageRange نفسه.
  • النطاق بعد النهاية خطأ، لا تقليم. إذا تجاوزت نهاية نطاق عدد صفحات المصدر، رفع split()PageLayoutException؛ ولا يقصّ النطاق بصمت إلى آخر صفحة أبدًا. افحص عدد الصفحات أولًا إذا كانت نطاقاتك مزوّدة من المستدعي.
  • يحتفظ splitEvery() بالباقي. يحمل الجزء الأخير ما تبقّى من صفحات، فمستند من 7 صفحات مقسّم كل صفحتين يعطي أربعة أجزاء: ثلاثة من صفحتين وواحد من صفحة. يجب أن يكون $pagesPerSegment 1 على الأقل، وإلا حصلت على InvalidArgumentException.
  • قائمة النطاقات الفارغة مرفوضة. يرفع split() مع $ranges === []InvalidArgumentException. ابنِ نطاقًا واحدًا على الأقل قبل استدعائه.
  • الحدود ترفع بدلًا من البتر. يرفع تجاوز maxBytes أو maxRangesInvalidArgumentException. ولا يعالج المُقسِّم مدخلًا مفرطًا جزئيًا أبدًا، فاضبط كلا الحدّين لحملك.
  • المصادر المشفّرة والموقّعة والحاملة لنموذج تفشل بإغلاق. يرفع مصدر مشفّر (لا يمكن نسخه دون المفتاح)، أو مصدر موقّع رقميًا (تعيد الترقيم تُبطل نطاق بايتات التوقيع)، أو مصدر يحمل نموذجًا تفاعليًا (قد تقع أدوات حقل على صفحات مُسقَطة فتتيتّم) ‏UnsupportedSourceDocumentException. يرفض المُقسِّم بدلًا من إصدار مستند تالف أو مُخترَق. وتقسيم مستند نموذج قيد معروف في هذا الإصدار.
  • UnsupportedSourceDocumentException تقيم تحت فضاء الأسماء ⁨Merge.⁩ اسمها المؤهّل بالكامل NextPDF\Document\Merge\UnsupportedSourceDocumentException. ذلك المسار Merge في صفحة تقسيم ليس خطأ نسخ ولصق: إنه استثناء رفض مستند المصدر الوحيد المشترك الذي ترفعه واجهتا الدمج والتقسيم كلتاهما حين لا يمكن نسخ مصدر بأمان. استورده من ذلك الفضاء.
  • الإخراج بنيويًا جديد، لا مستقرّ البايتات. كل جزء مستند جديد له فهرسه وشجرة صفحاته ومقطورته الخاصة. تشغيلان على المدخل نفسه متساويان بنيويًا، لكن غير مضمونَي تطابق البايتات — ومن هنا ملف تعريف إعادة الإنتاج structural.

التقسيم خطّي في عدد الصفحات المنسوخة عبر كل النطاقات. ويهيمن على العمل تحليل المصدر ونسخ إغلاق كائنات كل نطاق، لا مسك المُقسِّم لدفاتره. يُحتفظ بالمصدر في الذاكرة بوصفه سلسلة، وتُحتفظ بايتات كل جزء حتى تكتبها، فيتتبع الذروة في الذاكرة حجم المصدر زائدًا أكبر نطاق تنتجه. وتُبقي حارسة maxBytes جانب المصدر من تلك الذروة محدودًا. ولخطوط الأنابيب عالية الحجم، اضبط maxBytes وmaxRanges على أصغر قيم يحتاجها حملك، فيفشل مدخل مشوّه أو مفرط سريعًا بدلًا من استنزاف الذاكرة.

يجري التقسيم داخل العملية؛ فلا تغادر بايتات أي مستند المضيف، ولا يُجرى نداء شبكي. عامِل كل ملف ⁨PDF⁩ مصدر بوصفه مدخلًا غير موثوق:

  • أبقِ الحدود ضيّقة.maxBytes وmaxRanges خط دفاعك الأول ضد مدخل حجب الخدمة. لأي واجهة تقبل تحميلات، اضبطهما على سقفك الحقيقي، لا على القيم الافتراضية السخيّة.
  • افرز قبل أن تقسّم. يفشل بإغلاق مصدر مشفّر أو موقّع، لكن يمكنك كشف تلك الحالات أبكر. مرّر المدخلات غير الموثوقة عبر مفتّش ⁨Core⁩ أولًا. انظر تحليل ملف ⁨PDF⁩ وفحصه لمسح محدود يعلّم التشفير والتوقيعات وعلامات الخطر قبل معالجة أثقل.
  • لا تُقحِم مدخل المستخدم في مسار أبدًا. تكتب هذه الوصفة إلى دليل ثابت أو إلى قناة الكتاب الجانبية. اشتقّ مسارات الإخراج وأسماء الأجزاء من قيم يتحكّم بها الخادم، لا من حقل طلب، لتفادي اجتياز المسارات.
  • لا أسرار في الإخراج. لا تكتب ملفات الأجزاء إلى موقع، أو باسم، يكشف معرّفات داخلية لعميل لا ينبغي أن يراها.

لا تدّعي هذه الوصفة أي ادعاء معياري خاص بها. فهي تفكّك مستندًا واحدًا عبر واجهة التقسيم في ⁨Core⁩ وتفحص سلامة كل جزء بفحص ترويسة %PDF في SplitDocument::isValid() — فحص وجود بأن المُقسِّم أصدر ملف ⁨PDF،⁩ لا تحقق من المطابقة أو من بنية المستند. وبنى شجرة الصفحات والمراجع التقاطعية التي يعيد PdfSplitter بناءها لكل جزء هي بنى ⁨PDF 2.0⁩ الموصوفة في مرجع /modules/core/document/ ‏(⁨ISO 32000-2:2020⁩، جدول المراجع التقاطعية §⁨7.5.4⁩، شجرة الصفحات §⁨7.7.3⁩). لقراءة بنيوية لأي مستند مدخل أو إخراج، تشمل الإصدار وعدد الصفحات والتشفير وأعلام التوقيع، استخدم مفتّش ⁨Core⁩ الموثّق في تحليل ملف ⁨PDF⁩ وفحصه.