مقال تقني

صناديق PDFlibPas: افتراضات TrimBox و BleedBox و CropBox

حين تفتقر صفحة PDF إلى TrimBox يكون الـ TrimBox الفعّال لها هو الـ CropBox الخاص بالصفحة، وحين يغيب الـ CropBox أيضاً يكون هو الـ MediaBox. و BleedBox و ArtBox يتبعان القاعدة نفسها. تطبق PDFlibPas، مكتبة PDF لـ Delphi، سلسلة الافتراضات هذه بانسجام في GetPageBox و HasPageBox و CapturePageEx منذ v3.539.44، وتتجاهل صناديق الإنتاج الموضوعة على عقدة /Pages، لأن ISO 32000-1 لا يسمح لها بالتوريث

هذا يبدو حاشية سفلية حتى تجمّع عملاً على ورقة المطبعة. تخيَّل متن كتاب بـ MediaBox من 6.25 × 9.25 بوصة، و CropBox مضبوطاً على القص 6 × 9 بوصة، وبلا TrimBox، لأن من صدّره لم يخطر بباله أن يكتب واحداً. اطلب صندوق القص فتحصل على صندوق الوسائط بدلاً منه، وكل خلية على ورقة مطبعتك تسحب ثُمن بوصة من bleed و slug إلى جارتها. كان في PDFlibPas أعطاب في هذه المنطقة بالذات، صُححت في v3.539.42 و v3.539.44، وطريقة إصلاحهما تقول شيئاً عن كيفية تنفيذ دلالات صناديق الصفحات في أي مكتبة PDF

أي صندوق يسري حين لا تملك الصفحة TrimBox؟

الجواب سلسلة افتراضات ثابتة من ISO 32000-1 §14.11.2: يفترض الـ CropBox الـ MediaBox، ويفترض كل من BleedBox و TrimBox و ArtBox الـ CropBox. ولا شيء يفترض الـ MediaBox مباشرة إلا الـ CropBox. فصفحة تعرف MediaBox وحدها لها خمسة صناديق متطابقة، وصفحة تعرف MediaBox مع CropBox لها أربعة صناديق تساوي الـ CropBox

الصندوققيمة BoxType في PDFlibPasالافتراضي عند الغيابقابل للتوريث من /Pages
MediaBox1لا شيء، المدخل إلزامينعم
CropBox2MediaBoxنعم
BleedBox3CropBoxلا
TrimBox4CropBoxلا
ArtBox5CropBoxلا

وسلسلة الخطوتين مهمة لأن الـ CropBox نفسه قد يكون موروثاً. فالـ TrimBox الفعّال لصفحة بلا TrimBox وبلا CropBox خاص بها هو الـ CropBox لأقرب سلف يملك واحداً، وفشل ذلك يكون الـ MediaBox الموروث. ويضيف القسم قاعدة أخرى سهلة النسيان: صناديق القص والbleed والقص والفن لا ينبغي أن تمتد بعد صندوق الوسائط، وإن فعلت فهي فعلياً تنحصر في تقاطعها معه. تبلّغ PDFlibPas كل صندوق كما هو مخزن في الملف، فمُتحقِّق يعالج دخلاً غير موثوق عليه هو نفسه أن يقيّد مقابل الـ MediaBox

سلسلة افتراضات صناديق صفحات PDFlibPas حيث يفترض الـ CropBox الـ MediaBox ويفترض كل من BleedBox و TrimBox و ArtBox الـ CropBox، مرسومة بجانب متن كتاب بـ MediaBox من 450 في 666 نقطة و CropBox من 432 في 648 نقطة يصير القص الفعّال حين لا يوجد TrimBox
لا شيء يفترض الـ MediaBox مباشرة إلا الـ CropBox، فصفحة بـ MediaBox وحدها لها خمسة صناديق متطابقة

أي خصائص الصفحات يستطيع تمريرها عقدة /Pages؟

أربع بالضبط: ‏Resources و MediaBox و CropBox و Rotate. يعرّف ISO 32000-1 §7.7.3.4 توريث الخصائص، ويعلّم الجدول 30 تلك المدخلات الأربعة فقط لكائن الصفحة بوصفها قابلة للتوريث. أما BleedBox و TrimBox و ArtBox فمنشودان لصفحة الورقة. فـ TrimBox مكتوب في عقدة /Pages ليس قيمة موروثة؛ إنه مفتاح غير قياسي يتجاهله قارئ ملتزم

ملفات غير قياسية كهذه موجودة، عادة بـ TrimBox وحيد على عقدة جذر شجرة الصفحات اختصاراً لعبارة "كل صفحة لها هذا القص". ويبدو الاختصار صائباً في أي أداة تمشي في /Parent عن كل مفتاح، وهذه هي المشكلة: صار الملف يعني شيئين بحسب من يقرؤه. قارئ يتبع المواصفة لا يرى TrimBox فيستخدم الـ CropBox، وقارئ يورّث كل شيء يرى قيمة الأب. وفي مسار ما قبل الطباعة ينتهي هذا اللبس على ورقة المطبعة

توريث شجرة صفحات PDFlibPas حيث تمشي Resources و MediaBox و CropBox و Rotate فقط عبر عقدة Pages، فـ TrimBox متوقفة على الجذر مفتاح غير قياسي يتجاهله القراء الملتزمون؛ وقبل v3.539.44 ورثه مسارا كود مستقلان وبلّغا مقاسَي قص مختلفين لمستند واحد
الملف يعني شيئين بحسب من يقرؤه، وفي مسار ما قبل الطباعة يهبط ذلك اللبس على ورقة المطبعة

وسير عمل PDF/X ‏(ISO 15930) يعتمد على الـ TrimBox للمقاس النهائي، وتتطلب ملفات تعريف PDF/X أن تصرّح كل صفحة بـ TrimBox أو ArtBox. وصندوق متوقف على عقدة /Pages لا يحقق ذلك الشرط، لأن المفتاح لا يبلغ كائن الصفحة قط. وعلى preflight أن يشير إلى تلك الملفات لا أن يقرأها بهدوء بطريقة أو بأخرى

ماذا أخطأت PDFlibPas قبل v3.539.44؟

كان في PDFlibPas ثلاثة أعطاب منفصلة، كلها في الفجوة بين ما تقوله المواصفة وما فعله مسارا كود مستقلان. صُحح الأول في v3.539.42 والآخران في v3.539.44

صناديق الإنتاج افترضت الـ MediaBox أثناء الالتقاط

قبل v3.539.42 كانت الروتين الداخلي الذي يجهز صفحة للالتقاط (ينسخ المدخلات الموروثة على الصفحة ويملأ الصناديق المفقودة) يعطي BleedBox و TrimBox و ArtBox قيم الـ MediaBox حين تكون غائبة. و CapturePageEx بالخيارات من 2 إلى 4 يقرأ مستطيله المحيط من تلك المدخلات المملوءة بالضبط، فعلى صفحة تعرف CropBox وحده كان طلب صندوق القص يلتقط صندوق الوسائط كله. كان GetPageBox يطبق افتراض الـ CropBox أصلاً، وقال مرجع CapturePageEx دائماً إن صندوق القص يستخدم حين يكون الصندوق المطلوب مفقوداً؛ فكان كود الالتقاط مخالفاً للاثنين. ومنذ v3.539.42 تفترض صناديق الإنتاج الثلاثة الـ CropBox الخاص بالصفحة، وهو في تلك المرحلة موجود على الصفحة أصلاً (خاصها أو منسوخ من سلف أو مملوء من الـ MediaBox)، ولا يرجع إلى الـ MediaBox إلا الـ CropBox نفسه

مسارا توريث، وقاعدة دلالية واحدة

كان العطب الثاني هو التوريث غير القياسي ذاته، وكان الجزء الدقيق أن PDFlibPas تفاضل الصناديق عبر مسارين مستقلين. استعلامات الصناديق ‏(GetPageBox و HasPageBox) مشت في سلسلة /Parent عبر مساعد واحد، والالتقاط مشى فيها عبر مساعد محلي منفصل. وكلاهما ورث كل مفتاح، صناديقَ الإنتاج ضِمن ذلك. إصلاح واحد منهما فقط كان سيصنع تناقضاً داخل مستند واحد: مع TrimBox عرضه 180 نقطة على عقدة /Pages و CropBox عرضه 380 نقطة على الصفحة، ما زال GetPageBox سيبلّغ عن عرض قص 180 بينما بنى CapturePageEx فورماً بعرض 380. وفي v3.539.44 يقيّد كلا المسارين مشيَ /Parent بالمفاتيح الأربعة القابلة للتوريث، وتُقرأ صناديق الإنتاج من الورقة فقط، ويبقى مدخل الأب الشارد في الملف سليماً لا محذوفاً ولا معاد كتابته

رموز إعادة HasPageBox لدى PDFlibPas صفر وواحد واثنان مع احتساب المصفوفتين المباشرة وغير المباشرة موروثتين منذ v3.539.44، وبجانب خيارات CapturePageEx من صفر إلى أربعة حيث يرجع BleedBox و TrimBox و ArtBox إلى الـ CropBox بدل الـ MediaBox منذ v3.539.42
مدخلا تنفيذ لقاعدة مواصفة واحدة يُصلَحان معاً ويُختبران كمصفوفة من 18 سيناريو، مع اتفاق الاستعلام والالتقاط على كل ملف

فاتت HasPageBox المصفوفات الأبوية المباشرة

يعيد HasPageBox القيمة 0 حين لا تملك الصفحة صندوقاً من النوع المطلوب، و 1 حين تملك الصفحة صندوقها الخاص (مخزناً مباشرة أو عبر مرجع غير مباشر)، و 2 حين يكون MediaBox أو CropBox موروثاً من سلف. كان الكود القديم يعيد 2 فقط حين تكون القيمة الموروثة مرجعاً غير مباشر، فمصفوفة موروثة مباشرة كانت تعيد 0. يفصل الإصلاح فك المرجع عن اختبار المصفوفة، والتمثيلان يعيدان الآن 2. ومنذ v3.539.44 لا يستطيع HasPageBox لـ BleedBox أو TrimBox أو ArtBox أن يعيد إلا 0 أو 1

ويتعمم الدرس أبعد بكثير من صناديق الصفحات. حين يكون لدى دلالة مواصفة واحدة مدخلا تنفيذ في مكتبة، فأصلحهما معاً واختبرهما كمصفوفة لا بملف طريق سعيد واحد. يعبر مجموعة انحدار PDFlibPas تمثيلَي صندوق أبويين (مصفوفة مباشرة وغير مباشرة) بثلاث حالات ورقية (غائبة، مصفوفة مباشرة، مصفوفة غير مباشرة) وثلاثة خيارات التقاط ‏(bleed، trim، art)، فينتج 18 سيناريو، وكل واحد يفحص نتيجة الاستعلام والحدود الملتقطة وتوريث MediaBox و CropBox المشروع ومدخل الأب غير الممسوس

كيف أقرأ الـ TrimBox الفعّال في Delphi؟

استدعِ GetPageBox(4, Dimension) على الصفحة المختارة. تطبق PDFlibPas سلسلة الافتراضات عنك، فالنتيجة هي الـ TrimBox الفعّال سواء امتلكت الصفحة واحداً أو لا. وقُرنها بـ HasPageBox حين تحتاج أن تعرف من أين جاءت القيمة، وهو ما يفعله تقرير preflight عادة

uses
  System.SysUtils, PDFlibrary;

const
  BOX_CROP   = 2;
  BOX_TRIM   = 4;
  DIM_LEFT   = 0;
  DIM_WIDTH  = 2;
  DIM_HEIGHT = 3;
  DIM_BOTTOM = 5;

function DescribeTrim(Lib: TPDFlib; Page: Integer): string;
var
  Source: string;
begin
  Lib.SelectPage(Page);
  if Lib.HasPageBox(BOX_TRIM) = 1 then
    Source := 'own TrimBox'
  else if Lib.HasPageBox(BOX_CROP) <> 0 then   // ‏1 = خاص، ‏2 = موروث
    Source := 'defaulted to the CropBox'
  else
    Source := 'defaulted to the MediaBox';
  Result := Format('page %d: trim %.2f x %.2f pt at (%.2f, %.2f), %s',
    [Page,
     Lib.GetPageBox(BOX_TRIM, DIM_WIDTH),
     Lib.GetPageBox(BOX_TRIM, DIM_HEIGHT),
     Lib.GetPageBox(BOX_TRIM, DIM_LEFT),
     Lib.GetPageBox(BOX_TRIM, DIM_BOTTOM),
     Source]);
end;

var
  Lib: TPDFlib;
  Page: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('interior.pdf', '') = 1 then
      for Page := 1 to Lib.PageCount do
        Writeln(DescribeTrim(Lib, Page));
  finally
    Lib.Free;
  end;
end.

يعمل GetPageBox و SetPageBox كلاهما بإعدادات الإحداثيات الحالية للمستند. وتجرى الأمثلة هنا بالافتراضات: أصل 0 ‏(أسفل اليسار، مطابق لفضاء مستخدم PDF) والنقاط وحدة قياس، فبُعد Top هو الحافة العليا مقاسةً صعوداً من أسفل الصفحة. وبعد SetOrigin(1) يُقاس بُعدا Top و Bottom نزولاً من أعلى الصفحة بدلاً من ذلك، وبعد SetMeasurementUnits(1) تعود كل قيمة بالمليمترات. أما العرض والارتفاع فلا يتعلقان بالأصل

إيجاد صناديق الإنتاج المتروكة على عقد /Pages

منذ v3.539.44 لم يعد API الصناديق يرى TrimBox على عقدة /Pages، وهذا صائب، لكن أداة preflight تريد عادة أن تبلّغ عن ملف كهذا لا أن تقرأه بصمت بالطريقة المواصفية. عقد شجرة الصفحات كائنات عادية، فيستطيع API الكائنات منخفض المستوى أن يجدها: امشِ أرقام الكائنات حتى GetMaxObjectNumber، واقرأ كل واحد بـ GetObjectToString، وابحث عن قاموس /Pages يحمل مفتاح صندوق إنتاج. والنصف الثاني من الفحص هو الاختبار لكل صفحة الذي يهتم به PDF/X، ويجيب عنه HasPageBox الآن بالطريقة التي كان سيلجأ إليها مُتحقِّق PDF/X، لأن TrimBox أبوي لم يعد يُعَدّ

procedure PreflightTrim(Lib: TPDFlib; Log: TStrings);
const
  ProductionKeys: array[0..2] of string = ('/BleedBox', '/TrimBox', '/ArtBox');
var
  ObjNum, K, Page, Missing: Integer;
  Src: string;
begin
  // ‏1. صناديق إنتاج على عقد شجرة الصفحات: غير قياسية ومُتجاهلة
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // الأرقام الحرة لا تعيد نصاً
    Src := string(Lib.GetObjectToString(ObjNum));
    if Pos('/Type /Pages', Src) = 0 then
      Continue;
    for K := Low(ProductionKeys) to High(ProductionKeys) do
      if Pos(ProductionKeys[K] + ' ', Src) > 0 then
        Log.Add(Format('object %d: %s on a /Pages node is not inheritable',
          [ObjNum, ProductionKeys[K]]));
  end;

  // ‏2. ‏PDF/X: كل صفحة تحتاج TrimBox أو ArtBox خاصاً بها
  Missing := 0;
  for Page := 1 to Lib.PageCount do
  begin
    Lib.SelectPage(Page);
    if (Lib.HasPageBox(4) = 0) and (Lib.HasPageBox(5) = 0) then
    begin
      Inc(Missing);
      Log.Add(Format('page %d: no TrimBox or ArtBox', [Page]));
    end;
  end;

  // ‏3. إصلاح اختياري: قص 6 x 9 بوصة داخل صندوق وسائط 6.25 x 9.25 بوصة
  //    ‏(نقاط، أصل أسفل اليسار: ‏Left، ‏Top، ‏Width، ‏Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

مطابقة النص فحص عملي لا محلل. إنها تعتمد على تسلسل PDFlibPas لكل مدخل قاموس بمفتاح ومسافة وقيمة، وهو ما يسري على الكائنات المقروءة عائدةً عبر GetObjectToString. وخطوة الإصلاح تستحق قراراً لا انعكاساً: قيمة الأب الشارد قد تكون ما قصده المؤلف تماماً، لكن أكّدها مقابل أمر الشغل قبل أن تجعلها رسمية. يطبق SetPageBoxRange بمدى فارغ الصندوقَ على كل الصفحات ويعيد عدد الصفحات المحدّثة. وحين يكون صندوق الصفحة القائم مصفوفة غير مباشرة قد تشترك فيها صفحة أخرى أو عقدة /Pages، يعطي SetPageBox تلك الصفحة مصفوفة مباشرة جديدة بدل إعادة كتابة الكائن المشترك. وضبط BleedBox أو TrimBox أو ArtBox يرفع أيضاً مستنداً غير مقفل إلى PDF 1.3، الإصدار الذي قدم تلك المدخلات

تجميع الصفحات على الـ TrimBox بـ CapturePageEx

يحوّل CapturePageEx(Page, 3) صفحةً إلى Form XObject صندوقه المحيط هو الـ TrimBox الفعّال للصفحة، ويضع DrawCapturedPage ذلك الفورم على صفحة أخرى بأي مقاس. ومنذ v3.539.42 يعطيك الخيار 3 على صفحة بلا TrimBox الـ CropBox كما يصف المرجع، بدل الـ MediaBox بكل slug فيه

خاصيتا الالتقاط تشكلان الكود. الالتقاط مدمّر: الصفحة الملتقطة تُزال من المستند، ولا يستطيع المستند أن يهبط إلى صفر صفحات أبداً، فألحق أول ورقة مخرجات قبل أن تلتقط شيئاً. والالتقاط يعمل داخل مستند واحد فقط، فاسحب كل المدخلات إلى مستند واحد أولاً؛ وتنطبق تقنيات التجميع والتشبيك بين مصادر PDF في تمريرة واحدة مباشرة

procedure ImposeTwoUp(const InFile, OutFile: string);
var
  Lib: TPDFlib;
  Captures: array of Integer;
  SourceCount, I: Integer;
  TrimW, TrimH: Double;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(InFile, '') <> 1 then
      raise Exception.Create('Cannot open ' + InFile);
    SourceCount := Lib.PageCount;

    // حجم القص الفعّال للصفحة 1 (يفترض هذا التخطيط قصاً موحداً)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // ألحق الورقة الأولى واضبط حجمها؛ ‏NewPage تختار الصفحة الجديدة
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // كل التقاط يزيل الصفحة 1 فتصعد صفحة المصدر التالية
    SetLength(Captures, SourceCount);
    for I := 0 to SourceCount - 1 do
    begin
      Captures[I] := Lib.CapturePageEx(1, 3);   // ‏3 = ‏TrimBox
      if Captures[I] = 0 then
        raise Exception.CreateFmt('Capture of source page %d failed', [I + 1]);
    end;

    // لم يبق إلا الورقة: صفحتان مقصوصتان لكل ورقة جنباً إلى جنب
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // بحجم الورقة الحالية نفسه
      // الأصل الافتراضي: ‏Top هي الحافة العليا مقاسة من الأسفل
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

الالتقاط على أساس القص يقص كل ما هو خارج الـ TrimBox، وهذا ما تريده لبرهان رقمي أو لتخطيط قطع-وتكديس. ولورقة مطبعة ستُقص بعد الطباعة، التقط بالخيار 2 لينجو الـ bleed، وباعد الخلايا بعرض الـ bleed. ولأن الالتقاط يزيل صفحات المصدر، تفقد الإشارات المرجعية والروابط التي كانت تشير إليها أهدافها، فاجمع في ملف مخرجات منفصل بدل تحرير مستند ما زلت تحتاج تنقله؛ وتغطي ذلك استبدال الصفحات دون كسر الإشارات المرجعية جانبَ جراحة الصفحات ذاك

وحين لا بد أن يبقى المصدر سليماً، يأخذ ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) قيم الخيارات من 0 إلى 4 نفسها (مرّر Lib.SelectedDocument للمستند الحالي)، ويترك شجرة صفحات المصدر كما هي، ويطبع دوران الصفحة الموروث في مصفوفة الفورم، ويعيد مقبضاً يقبله DrawCapturedPage. لا يفك CapturePageEx ‏/Rotate، فالدخل المدور يحتاج تلك الخطوة أولاً، وتُظهر تسطيح دوران الصفحة دون كسر صناديق الصفحات ماذا يحدث لكل صندوق حين تفعل. وتحذير واحد لدخل قد يحمل صناديق إنتاج على عقد /Pages: مسار الاستيراد تفاضل صندوقه عبر بحث السلف الخاص به، منفصلاً عن المسارين المتوائمين في v3.539.44، فافحص HasPageBox(4) على صفحة المصدر أولاً ومرّر الخيار 1 ‏(CropBox) حين يعيد 0. يبقي ذلك النتيجة مربوطةً بالمواصفة لا بطريقة كتابة الملف المصادفة

مرجع سريع لصناديق الصفحات

  • الـ CropBox الفعّال: الـ CropBox الخاص بالصفحة، وإلا أقرب CropBox موروث، وإلا الـ MediaBox الفعّال ‏(ISO 32000-1 §14.11.2)
  • الـ BleedBox والـ TrimBox والـ ArtBox الفعّالة: مدخل صفحة الورقة الخاص، وإلا الـ CropBox الفعّال
  • تفترث Resources و MediaBox و CropBox و Rotate وحدها من عقد /Pages ‏(§7.7.3.4، الجدول 30)؛ وصناديق الإنتاج على عقد /Pages تُتجاهل
  • GetPageBox(BoxType, Dimension): ‏BoxType ‏1 ‏MediaBox، ‏2 ‏CropBox، ‏3 ‏BleedBox، ‏4 ‏TrimBox، ‏5 ‏ArtBox؛ و ‏Dimension ‏0 ‏Left، ‏1 ‏Top، ‏2 ‏Width، ‏3 ‏Height، ‏4 ‏Right، ‏5 ‏Bottom
  • HasPageBox(BoxType): ‏0 لا صندوق، ‏1 صندوق الصفحة الخاص (مباشر أو غير مباشر)، ‏2 ‏MediaBox أو CropBox موروث (مباشر أو غير مباشر)
  • CapturePageEx(Page, Options): ‏0 ‏MediaBox، ‏1 ‏CropBox مع تراجع إلى MediaBox، ‏2 إلى 4 ‏BleedBox أو TrimBox أو ArtBox مع تراجع إلى CropBox
  • رقِّ إلى v3.539.44 أو أحدث لافتراضات وتوريث متسقَين عبر استعلامات الصناديق والالتقاط

صناديق الصفحات هي حيث تقابل الافتراضات الهادئة لـ PDF سماحاتِ ما قبل الطباعة المقاسة بكسور مليمتر، والمكتبة إما أن تطبق تلك الافتراضات بالطريقة نفسها في كل مكان أو تسلّمك جوابين لسؤال واحد. و API الصناديق والالتقاط و Form XObject الكامل موثق على صفحة منتج PDFlibPas PDF Library for Delphi