احتوى الشريط الأول على الرسم كاملًا مضغوطًا في شريط واحد، وعادت الأشرطة الخمسة التي تلته فارغة. كان ذلك هو التصدير الشريطي القديم، وأصلحه PDFiumPas في v3.66.0: إذ تمرر RenderPageBanded الآن عرض الهدف وارتفاعه الكاملين إلى FPDF_RenderPageBitmap في كل شريط، مع إزاحة رأسية سالبة، ولذلك يكتب القص الأصلي الصفوف الخاصة بالشريط الحالي فقط بينما تحتفظ الصفحة بهندسة إحداثياتها الكاملة. وحالة الاستخدام وراء كل ذلك مملة ولا يمكن تجنبها. يسلمك أحدهم مخططًا بحجم E أو صفحة بانوراما ملتحمة ويريد نسخة raster بدقة 600 DPI. وتكون ورقة ISO A0 عند 600 DPI بحجم 19866 × 28086 بكسل، بينما تتجاوز صورة bitmap وجهة 32 بت بهذا الحجم قليلًا 2 GB من الذاكرة المتجاورة. ويفشل هذا التخصيص ببساطة في Delphi ذي 32 بت. أما في 64 بت فينجح بما يكفي لتحويل الفشل إلى مشكلة عميل بدل مشكلة اختبار. ويوجد التصيير الشريطي حتى يكون أعلى تخصيص شريطًا واحدًا لا صفحة واحدة
لماذا احتوى كل شريط على الصفحة كاملة؟
خلط الكود القديم بين زوجين مختلفين من المعاملات في استدعاء تصيير الصفحة في PDFium. يأخذ FPDF_RenderPageBitmap القيم start_x وstart_y وsize_x وsize_y، حيث يقول زوج الحجم إلى أي حجم ينبغي تحجيم الصفحة كاملة، بينما يقول زوج البدء أين تقع الصفحة المحجّمة داخل bitmap الوجهة. وكانت حلقة الأشرطة قبل v3.66.0 تستدعي مساعد المكتبة RenderPage مع أعلى الشريط كإزاحة للوجهة وارتفاع الشريط كارتفاع للصفحة. فمر هذان الرقمان مباشرة إلى الاستدعاء الأصلي، ولذلك حوّل PDFium الصفحة كاملة إلى مستطيل لا يتجاوز BandHeight صفًا، ثم رسمها عند y = BandTop داخل bitmap لا يتجاوز بدوره BandHeight صفًا. والنتيجة هي بالضبط ما تتوقعه بمجرد رؤيتها. تلقى الشريط صفر الصفحة كاملة مضغوطة رأسيًا إلى ارتفاع الشريط. وتلقى كل شريط لاحق الصفحة المضغوطة نفسها مدفوعة تحت الحافة السفلية لـbitmap، فعاد كملء خلفية. ويختبئ الخطأ في الحالة الوحيدة التي تستخدمها معظم اختبارات الدخان، وهي صفحة يكون ارتفاع تصييرها أصغر من ارتفاع الشريط، إذ يوجد عندها شريط واحد ويتوافق الخطأ الهندسي مصادفة مع الصحيح. وكل ما يزيد على شريط واحد يكشفه فورًا
ما الذي تضمنه الإزاحة السالبة؟
يوجه التنفيذ المصحح كل شريط عبر RenderTile، وهو الموضع الوحيد في المكوّن الذي فهم التمييز أصلًا. يأخذ RenderTile أصل tile بإحداثيات بكسلات الصفحة الكاملة، مع PageWidth وPageHeight منفصلين، ويمرر إلى PDFium القيمتين -Left و-Top مع إبقاء حجم الصفحة دون تغيير. وتزحزح سلبية الإزاحة الصفحة ذات الحجم الكامل إلى الأعلى حتى يقع الشريط المطلوب في الصف صفر من bitmap الوجهة؛ ثم يقص PDFium أصلًا مقابل حدود bitmap، ولذلك لا يجري تصيير شيء خارج الشريط. ويبقى تحويل الصفحة إلى الجهاز الموصوف في ISO 32000-1 البند 8.3.2 مطابقًا من الشريط الأول إلى الأخير، وهذه هي الفكرة كلها: فالشريط N مطابق بتًا ببت للصفوف من BandTop إلى BandTop + h من تصيير صفحة كاملة واحد، وتؤكد مجموعة التراجع ذلك بكسلًا بكسل مقابل ناتج RenderPage بالأبعاد نفسها
// شريط واحد يدويًا. لا يزيد ارتفاع bitmap الوجهة عن BandHeight،
// لكن حجم هدف الصفحة يبقى Width x Height كاملًا
Band := Pdf.RenderTile(0, BandTop, // أصل tile في بكسلات الصفحة
Width, BandHeight, // حجم bitmap الوجهة
Width, Height); // حجم الهدف للصفحة الكاملة
try
// يحمل Band الآن الصفوف BandTop .. BandTop + BandHeight - 1 من الصفحة
finally
Band.Free;
end;
وتكون API الشريط العامة حلقة callback. تعيد RenderPageBanded(Width, Height, BandHeight, BandCallback, Rotation, Options, Color) عدد الأشرطة التي صيرتها فعلًا، أو 0 عندما تُرفض المعاملات، وتحمل قفل تصيير المكوّن طوال التمريرة. ويكون توقيع callback هو TPdfBandCallback = function(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean of object. ويكون bitmap من نوع pf32bit وعرضه Width بكسل ولا يزيد ارتفاعه على BandHeight، ويُحرر فور عودة المعالج، ولذلك انسخ أي شيء تنوي الاحتفاظ به. ويوقف إرجاع False التمريرة بعد الشريط الحالي، وهو ما يمنحك نموذج الإلغاء التعاوني نفسه المستخدم في التصيير التدريجي القابل للإلغاء لـPDF في Delphi، لكن على مستوى الشرائط بدل مستوى استمرار PDFium
type
TBandSink = class
private
FCancelled: Boolean;
FRows: Integer;
public
function HandleBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
property Rows: Integer read FRows;
end;
function TBandSink.HandleBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
// يموت Bitmap عند عودة هذه الدالة، فاستهلكه هنا
Inc(FRows, Bitmap.Height);
Result := not FCancelled;
end;
// ...
Pdf.PageNumber := 1;
Bands := Pdf.RenderPageBanded(19866, 28086, 256, Sink.HandleBand);
بث PNG وTIFF من دون bitmap للصفحة كاملة
لا يفيد التصيير على شرائط إلا إذا كان المرمّز تسلسليًا أيضًا، ولذلك أضاف v3.66.0 RenderPageBandedToStream الذي يكتب PNG أو TIFF مباشرة في تدفق المستدعي. وتزرع TPdfBandedImageStreamOptions.Default ارتفاع شريط يبلغ 256 صفًا، ومستوى ضغط PNG يساوي 6، وMaxOutputBytes بقيمة 0، أي بلا حد. ويحمل TPdfBandedImageReport العائد الحقول Format وWidth وHeight وBandsRendered وBandsEncoded وRowsEncoded وPeakBandBytes وOutputBytes وCompleted. وPeakBandBytes هو الرقم الذي يهمك فعلًا عند تحديد حجم المهمة: فهو Width * BandHeight * 4، ولذلك تبلغ ذروة ورقة A0 السابقة نحو 19 MB لمخزن الشريط بدل 2 GB لمخزن الصفحة
مرمّز PNG ضيق عمدًا. فهو يصدر RGB8 ثابتًا، ويكتب IHDR بعمق بتات 8 ونوع لون 2، ثم يبني كل scanline بمرشح من النوع 0 (طريقة المرشح 0 في ISO/IEC 15948، نوع المرشح None)، ويدفعه عبر تدفق ضغط zlib الخاص بالمنصة. وتخرج البايتات المضغوطة في صورة كتل IDAT حاملة لـCRC ومكتوبة بالترتيب. والقيد المثير للاهتمام هو التدفق الموجود تحت طبقة deflate: فهو يجيب عن استعلامات الموضع لأن تدفق الضغط يطلبها، لكن أي محاولة seek حقيقية ترفع خطأ. وهذا مقصود. فبمجرد أن تكون كتلة IDAT وCRC الخاص بها على السلك لا سبيل للعودة لإصلاحهما، والـseek الصامت سيفسد ناتجًا لا يزال يبدو سليمًا بنيويًا
يكتب مرمّز TIFF ملف TIFF تقليديًا بترتيب little-endian، وعلامة ترتيب البايت II تليها القيمة السحرية 42، مع شريط واحد لكل band. تخرج البكسلات أولًا، ويُنشأ IFD ذو الإدخالات العشر في النهاية بعد معرفة إزاحات الأشرطة وأعداد البايتات. والضغط هو القيمة 1 للوسم 259، ولذلك لا يوجد ترميز entropy أصلًا: الحمولة هي بالضبط Width * Height * 3 بايتات، وPhotometricInterpretation هي RGB، وPlanarConfiguration chunky، بينما يسجل RowsPerStrip ارتفاع الشريط ويصف الشريط القصير الأخير بإدخال StripByteCounts خاص به. ولذلك يغير ارتفاع الشريط الذاكرة القصوى وعدد الأشرطة لكنه لا يغير حجم الناتج، وهي معلومة مفيدة قبل ضبطه. وإذا كنت تريد ملفات صغيرة بدلًا من ملفات غير فقدية، فيظل مسار الصفحة لكل صفحة في تحويل صفحات PDF إلى صور JPEG مع مكوّن PDFium VCL الأداة الأفضل
var
StreamOptions: TPdfBandedImageStreamOptions;
Report: TPdfBandedImageReport;
Output: TFileStream;
begin
StreamOptions := TPdfBandedImageStreamOptions.Default(pbifPng);
StreamOptions.BandHeight := 512;
StreamOptions.CompressionLevel := 6;
StreamOptions.MaxOutputBytes := Int64(256) * 1024 * 1024;
Output := TFileStream.Create('sheet-a0-600dpi.png', fmCreate);
try
Report := Pdf.RenderPageBandedToStream(Output, 19866, 28086,
StreamOptions);
finally
Output.Free;
end;
if not Report.Completed then
raise Exception.Create('Banded export stopped before the last row');
// Report.PeakBandBytes = 19866 * 512 * 4، لا 19866 * 28086 * 4
end;
أين يتوقف التصدير الشريطي؟
يحد سقفان الناتج، ويفشلان في موضعين مختلفين عمدًا. الأول هو ميزانية المستدعي: إذ يفرض MaxOutputBytes تدفق كتابة محدودًا يرفع EPdfError قبل أي كتابة تتجاوز الحد، ولذلك تكون الميزانية سقفًا صلبًا لا تقريرًا بعد الواقعة. والثاني بنيوي. يخزن TIFF التقليدي إزاحات الأشرطة كقيم 32 بت، ولذلك يتحقق BeginImage من Width * Height * 3 مع الرأس والدليل مقابل ذلك السقف، ويرفض المهمة قبل كتابة بكسل واحد؛ كما يجري الفحص نفسه مقابل MaxOutputBytes مقدمًا، لأن TIFF الذي لا تغطي ميزانيته حمولة بكسلاته الذاتية لا يستحق البدء. ولا يملك PNG حدًا مماثلًا، لأن كتل IDAT تسلسلية بحتة ولا يوجد جدول إزاحات 32 بت يمكن أن يفيض
كن واضحًا بشأن ما يتركه التصدير المتوقف وراءه. فعندما لا تصل التمريرة إلى الصف الأخير، تبقى Completed بالقيمة False، ويُفكك المرمّز باستخدام EndImage(False)، وهو ما يكتب عمدًا لا كتلة IEND الخاصة بـPNG ولا IFD الخاصة بـTIFF. ولذلك يكون الملف الجزئي غير صالح، وسيقول كل مفكك ذلك بدل صورة تبدو معقولة وتنقصها صفوف. ويُغلّف ذلك التنظيف بحيث لا يستطيع فشل ثانوي داخل EndImage استبدال الاستثناء الأصلي، وهذا هو الفرق بين أثر مكدس يسمي السبب الحقيقي وآخر يسمي عامل التنظيف. وإذا احتجت إلى استمرار قابل للبقاء، فأنشئ checkpoint لكل شريط داخل callback الخاص بك؛ وتنطبق هنا أيضًا تكتيكات التخزين المؤقت على مستوى الشريط في دليل ذاكرة تصيير PDFium في Delphi والتكبير
إدخال مرمّزك الخاص
عندما لا يكون PNG وTIFF هما الهدف، يأخذ RenderPageBandedToEncoder سليلًا من TPdfBandedImageEncoder ويقود الحلقة نفسها. ودورة الحياة صريحة وقصيرة: BeginImage(Width, Height)، ثم WriteBand(BandIndex, BandTopY, Bitmap) مرة لكل شريط بترتيب تصاعدي صارم، ثم EndImage(Completed)، مع تغذية GetBytesWritten إلى Report.OutputBytes. وترفض المرمّزات المضمنة الشريط الخارج عن الترتيب مباشرة بدل محاولة تخزينه، وينبغي لأي مرمّز تكتبه أن يفعل الشيء نفسه، لأن codec يعيد ترتيب الأشرطة بصمت ينتج ملفًا يفتح ويكذب. وهذه هي الوصلة المناسبة لبلاطات JPEG 2000، أو كاتب JPEG يتغذى بصف MCU واحد في كل مرة، أو تغذية مباشرة إلى spooler الطباعة
type
TCodecBandEncoder = class(TPdfBandedImageEncoder)
private
FNextBand: Integer;
FWritten: Int64;
public
procedure BeginImage(Width, Height: Integer); override;
function WriteBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean; override;
procedure EndImage(Completed: Boolean); override;
function GetBytesWritten: Int64; override;
end;
function TCodecBandEncoder.WriteBand(BandIndex, BandTopY: Integer;
Bitmap: TBitmap): Boolean;
begin
if BandIndex <> FNextBand then
raise EPdfError.Create('Bands must arrive in order');
Bitmap.PixelFormat := pf32bit;
// مرر Bitmap.ScanLine[0 .. Bitmap.Height - 1] إلى المرمّز هنا
Inc(FNextBand);
Result := True;
end;
فخ واحد في المصرّفات المتعددة يستحق المعرفة
تُكتب وحدة zlib بصورة مختلفة في كل سلسلة أدوات مدعومة: يستخدم Delphi XE5 وما بعده System.ZLib، ويستخدم FPC zstream، بينما يستخدم Delphi الأقدم ZLib مجردة. وهذا قدر روتيني من الترجمة الشرطية. لكن الفخ هو أن الثلاثة تصدر ثوابت لمستوى الضغط اسمها clNone وclDefault، فتتصادم مباشرة مع أعضاء TColor بالاسم نفسه في وحدة الرسوميات. وبمجرد ظهور وحدة zlib في عبارة uses الخاصة بالتنفيذ، قد يحل clNone غير المؤهل في كود التصيير إلى مستوى ضغط بدل لون من دون أي تشخيص. ويثبت PDFiumPas ذلك بأسماء مستعارة صريحة لثوابت الألوان، هي PdfGraphicsColorNone وPdfGraphicsColorDefault، مرتبطة مرة واحدة بثوابت الرسوميات المؤهلة بالكامل، ومستخدمة حيث يُقارن sentinel خلفية التصيير أو نظام الألوان. ثلاثة أسطر من الكود، وينتهي انجراف حل الرموز بين المصرّفات
يبدو التصيير الشريطي كأنه ميزة راحة إلى أن تواجه صفحة لا تتسع في RAM، وعندها يكون المسار الوحيد الذي يعمل. وتأتي هندسة الأشرطة المصححة، ومرمّزا PNG وTIFF التسلسليان، ووصلة المرمّز المخصص ضمن مكوّن PDFium لـDelphi، مع تشغيل مقارنة البكسلات الكاملة بين الشريط والصفحة في مجموعة التراجع عبر Delphi وLazarus وC++Builder