HotPDF چرخش صفحهٔ PDF را با THotPDF.FlattenLoadedPageRotation تخت میکند: این متد محتوای هر صفحهٔ چرخیده را در یک transform از جنس cm ساعتگرد میپیچد، هر page box ای که صفحه واقعاً دارد را بازنویسی میکند، هندسهٔ حاشیهنویسیها و ماتریسهای appearance و مقصدهای صریح و هندسهٔ ساختار tag شده را با همان زاویه میچرخاند و بعد /Rotate را روی 0 میگذارد. صفحه در viewer عین همان به نظر میرسد، اما دستگاه مختصاتش حالا راست است. همان لحظهای مهم میشود که یک ابزار پاییندستی، یک print RIP یا کد مهر زدن خودت به /Rotate بیتوجهی کند و چیزها را در user space خام جاگذاری کند
محرک معمولی یک اسکنر یا یک اپ کپچر موبایلی است که صفحات افقی را بهشکل رسانهٔ عمودی با /Rotate 90 مینویسد. هر viewer آنها را درست نشان میدهد، پس هیچکس متوجه نمیشود تا وقتی کسی شمارهٔ صفحه را در «پایین سمت راست» مهر بزند و بهشکل خوابیده در طول لبهٔ چپ فرود بیاید، یا یک مرحلهٔ imposition که فقط /MediaBox را میخواند برای یک صفحهٔ افقی یک جایگاه عمودی بچیند. تخت کردن از دور یک کار ماتریسی یکخطی به نظر میرسد. در عمل به پنج page box و سه نوع هندسهٔ حاشیهنویسی و مقصدهای لینک سند و درخت ساختار دست میزند و هر کدام قاعدهٔ خودش را در ISO 32000-1 دارد
/Rotate صفحهٔ PDF را به کدام سمت میچرخاند؟
/Rotate صفحه را برای نمایش و چاپ ساعتگرد میچرخاند، در مضاربی از 90 درجه (ISO 32000-1 §7.7.3.3، جدول 30). در 90 درجه لبهٔ چپ رسانه تبدیل به بالا و لبهٔ بالا تبدیل به سمت راست میشود، پس در یک device space با y رو-به-پایین نگاشت چنین است: X = (y - Bottom) * Scale و Y = (x - Left) * Scale. در 270 درجه لبهٔ راست تبدیل به بالا میشود. /Rotate همچنین یکی از تنها چهار attribute صفحهٔ ارثی است، کنار /Resources و /MediaBox و /CropBox (§7.7.3.4)، پس dictionary صفحهای که /Rotate خودش را ندارد همچنان میتواند توسط یک اجداد /Pages چرخانده شود. THotPDF.GetLoadedPageRotation زنجیرهٔ /Parent را میپیماید و نتیجه را در بازهٔ 0 تا 359 نرمال میکند، که همان مقداری است که میخواهی، نه کلید خام روی صفحه
جهت بهسادگی به شکلی اشتباه گرفته میشود که از تست جان سالم به در میبرد، و بیلدهای قبلی HotPDF دقیقاً همین را کردند. ماتریس قدیمی صفحه-به-دستگاه مؤلفههای y را برای 90 و 270 جابهجا میکرد، که بهجای چرخش یک بازتاب روی قطر تولید میکند: جهت ماتریس نسبت به حالت نچرخیده برعکس میشود. هر دو زاویه همچنان «چرخیده» به نظر میرسند، بیتمپ عرض و ارتفاع جابهجا شده دارد، و یک رفتوبرگشت از صفحه به نما و برعکس نقطهٔ شروع را برمیگرداند، پس چکهای ابعاد و تستهای رفتوبرگشت همه سبز میشوند. تنها چک قابلاتکا این است که نشانگر گوشه کجا فرود میآید، مقایسهشده پیکسلبهپیکسل با یک رندرکنندهٔ مرجع. چون مدل viewer و backend رندر SIMD و نگاشت هایلایت همان ماتریس را کپی کرده بودند، همه با هم اصلاح شدند و کد تخت کردن حالا از همان قرارداد ساعتگرد رندرکننده استفاده میکند
FlattenLoadedPageRotation چطور یک صفحه را بازنویسی میکند
FlattenLoadedPageRotation(PageRange, Info) هر صفحه در PageRange را که چرخش مؤثرش 90 یا 180 یا 270 است پردازش میکند و تعداد صفحاتی را که تخت کرده برمیگرداند. یک PageRange خالی یعنی همهٔ صفحات؛ وگرنه رشته از سینتکس معمولی یک-مبنا یعنی '1-3,7' استفاده میکند و شمارهٔ صفحهٔ خارج از بازه بهجای رد شدن exception بالا میدهد. content streamهای اصلی هرگز دوباره انکود نمیشوند. متد یک stream جدید شامل q 0 -1 1 0 -Bottom Width+Left cm (برای 90 درجه) را به ابتدای /Contents صفحه اضافه میکند، یک stream شامل Q را به انتها میچسباند و در نهایت یک /Rotate 0 صریح داخل dictionary صفحه مینویسد تا یک مقدار ارثی روی گرهٔ /Pages نتواند صفحه را بار دوم بچرخاند
var
Pdf: THotPDF;
Info: THPDFRotationFlattenInfo;
Flattened: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned-batch.pdf') > 0 then
begin
// '' یعنی همهٔ صفحات؛ صفحات در 0 درجه اسکن میشوند اما دست نخورده میمانند
Flattened := Pdf.FlattenLoadedPageRotation('', Info);
Writeln(Format('Scanned %d, flattened %d pages', [Info.ScannedPageCount, Info.FlattenedPageCount]));
Writeln(Format('Turned %d annotations, %d destinations, %d tagged geometry entries',
[Info.TransformedAnnotationCount, Info.TransformedDestinationCount,
Info.TransformedStructureGeometryCount]));
if Flattened > 0 then
Pdf.SaveLoadedDocument('scanned-batch-upright.pdf');
end;
finally
Pdf.Free;
end;
end;
رکورد THPDFRotationFlattenInfo ارزش لاگ شدن دارد نه دور انداختن. ScannedPageCount اندازهٔ بازه است، FlattenedPageCount با مقدار برگشتی برابر است، و سه شمارندهٔ Transformed... به تو میگویند آیا سند لینک یا بوکمارک یا هندسهٔ tag شدهای داشته که به صفحات چرخیده اشاره میکرده. یک بسته که هر فایلش صفر مقصد گزارش میکند عادی است؛ یک فایل PDF/UA تگشده که وقتی انتظار جعبههای محصورکنندهٔ figure را داری صفر هندسهٔ ساختار گزارش میکند سیگنالی است که باید دستی سراغش بروی
تخت کردن کدام page boxها را بازنویسی میکند و به چه ترتیبی؟
تخت کردن فقط جعبههایی را بازنویسی میکند که صفحه از قبل دارد و هر جعبه را قبل از نوشتن هر کدام میخواند. ترتیب مهم است به خاطر زنجیرهٔ پیشفرض: GetLoadedPageBox(PageIndex, pbCropBox, ...) وقتی صفحه /CropBox ندارد برمیگرداند /MediaBox را، و /BleedBox و /TrimBox و /ArtBox به CropBox برمیگردند (§14.11.2). یک نسخهٔ قبلی واقعاً یک بار یک جعبه را میخواند و transform و مینوشت. اول MediaBox را بازنویسی میکرد، بعد «CropBox» را میخواند، همان MediaBox از قبل چرخیده را پس میگرفت، بار دوم میچرخاندش و یک CropBox مینوشت که صفحه هرگز نداشت، که یک صفحهٔ افقی را به یک مربع برش میداد. قواعد ارث هم همینطور تقسیم میشوند: MediaBox و CropBox در طول زنجیرهٔ /Parent جستوجو میشوند، در حالی که Bleed و Trim و ArtBox فقط وقتی حساب میشوند که روی خود dictionary صفحه نشسته باشند، پس یک /TrimBox سرگردان روی گرهٔ /Pages غایب تلقی میشود و هرگز روی صفحه کپی نمیشود
procedure DumpPageGeometry(Pdf: THotPDF; PageIndex: Integer);
var
L, B, R, T: Single;
begin
Writeln('Effective /Rotate: ', Pdf.GetLoadedPageRotation(PageIndex));
if Pdf.GetLoadedPageBox(PageIndex, pbMediaBox, L, B, R, T) then
Writeln(Format('MediaBox [%g %g %g %g]', [L, B, R, T]));
// حتی بدون کلید /TrimBox هم True است: مقدار به CropBox و بعد MediaBox برمیگردد
if Pdf.GetLoadedPageBox(PageIndex, pbTrimBox, L, B, R, T) then
Writeln(Format('TrimBox [%g %g %g %g]', [L, B, R, T]));
// Letter از پیش تنظیم شده؛ GetLoadedPageVisibleBox در شکست خروجیها را دستنخورده میگذارد
L := 0; B := 0; R := 612; T := 792;
Pdf.GetLoadedPageVisibleBox(PageIndex, L, B, R, T);
Writeln(Format('Visible [%g %g %g %g]', [L, B, R, T]));
end;
آن هلپر را قبل و بعد از تخت کردن اجرا کن و اعداد خودشان را توضیح میدهند. برای یک صفحهٔ 90 درجه با MediaBox برابر [0 0 612 792]، MediaBox تختشده میشود [0 0 792 612]؛ هر جعبهٔ بازنویسیشده از همان چرخش ساعتگرد، نسبت به مبدأ MediaBox اصلی، عبور میکند، پس MediaBox جدید همیشه از مبدأ شروع میشود و بقیهٔ جعبهها موقعیتشان داخل آن حفظ میشود. GetLoadedPageVisibleBox چیزی را برمیگرداند که viewerها نمایش میدهند و پرینترها چاپ میکنند، یعنی CropBox بریدهشده به MediaBox و نرمالشده بهطوری که Left کوچکتر از Right باشد، و رندرکننده و export از SVG و viewer و مسیر چاپ HotPDF همه از همان جعبه استفاده میکنند. وقتی اندازهٔ صفحهای را میخواهی که آدم میبیند، GetLoadedPageVisibleBox را صدا بزن نه خواندن /MediaBox را
چرا وقتی فقط /Rect را میچرخانی حاشیهنویسیها میشکنند؟
حاشیهنویسیها میشکنند چون یک appearance stream مستقیم در /Rect کشیده نمیشود. طبق §12.5.5 اول viewer، /BBox فرم را با /Matrix اش transform میکند، بعد جعبهٔ محصورکنندهٔ آن نتیجه را مقیاس و منتقل میکند تا در /Rect جا بگیرد. فقط /Rect را بچرخان و یک مهر 200 در 40 در یک جایگاه 40 در 200 فشرده میشود، ناخوانا و خوابیده. پس FlattenLoadedPageRotation چرخش ساعتگرد صفحه را از سمت راست روی /Matrix هر appearance ضرب میکند (برای 90 درجه، [0 -1 1 0 0 0] در قرارداد بردار-سطر)، روی هر سه appearance یعنی /N و /R و /D و هر وضعیتی داخلشان. یک appearance stream میتواند بین چند حاشیهنویسی یا وضعیت مشترک باشد، پس هر stream دقیقاً یک بار در هر فراخوانی چرخانده میشود. تنها حالت بدون جواب تمیز streamی است که بین صفحاتی با چرخشهای متفاوت مشترک است؛ از اولین صفحهای که به آن میرسد پیروی میکند
دو قاعدهٔ دیگر فیلدهای فرم و یادداشتهای چسبان را سر جایشان نگه میدارند. درایهٔ /MK /R یک widget (§12.5.6.19) یک زاویهٔ پادساعتگرد است، پس زاویهٔ ساعتگرد صفحه از آن کم میشود، به پیمانهٔ 360؛ اگر این را رد کنی تولید دوبارهٔ appearance بعدی متن فیلد را در جهت اشتباه میکشد. حاشیهنویسیهایی که فلگ NoRotate دارند (موقعیت بیت 5، مقدار 16، §12.5.3) روی صفحهٔ چرخیده راست میمانند و دور گوشهٔ بالا-چپ /Rect شان میچرخند، پس تخت کردن عرض و ارتفاع و ظاهر ایستادهشان را نگه میدارد و فقط آن گوشه را به جایی میبرد که چرخش تعیین میکند. فراتر از حاشیهنویسیها، متد همچنین /QuadPoints و /Vertices و /L و /InkList را میچرخاند، مقصدهای صریحی را که صفحه را نام میبرند بازنویسی میکند (نقطههای /XYZ، مستطیلهای /FitR، و /FitH / /FitV که در 90 و 270 درجه جابهجا میشوند، §12.3.2.2)، و هندسهٔ tag شده مثل درایههای /BBox در attributeها را برای عناصر ساختاری که /Pg شان همان صفحه است transform میکند
تخت کردن چه چیزهایی را پوشش نمیدهد؟
تخت کردن یک بازنویسی هندسی از اشیای خودِ یک صفحه است و چند حالت آرام و بیسروصدا بیرون از آن میمانند
- صفحاتی که چرخش مؤثرشان از قبل 0 است یا MediaBox شان غایب است یا عرض یا ارتفاع صفر دارد، بدون خطا رد میشوند؛ مقدار برگشتی را با تعداد صفحاتی که انتظار تغییرشان را داشتی مقایسه کن
- Form XObjectهایی که از منابع صفحه ارجاع میشوند
/BBoxخودشان را در form space نگه میدارند، چونcmبیرونی از قبل آنها را میچرخاند؛ اسکن درخت ساختار فقط/Kو/Aرا دنبال میکند پس هرگز بار دوم وارد منابع صفحه یا حاشیهنویسیها نمیشود - مقصدها با اسکن کردن هر شیء غیرمستقیم بهازای هر صفحهٔ تختشده پیدا میشوند، پس سند بزرگی با صدها صفحهٔ چرخیده بهای آن پیمایش را بهازای هر صفحه میپردازد
- رندرکنندهٔ صفحهٔ HotPDF حاشیهنویسیها را نمیکشد، پس یک بررسی بصری از مهرهای چرخیده اول
FlattenLoadedAnnotationsلازم دارد
// appearanceها را داخل محتوا بپز تا رندرکننده بتواند نشانشان بدهد،
// بعد صفحهٔ 1 را قبل و بعد از حذف /Rotate اش رندر کن
Pdf.FlattenLoadedAnnotations('1');
Before := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Pdf.FlattenLoadedPageRotation('1', Info);
After := Pdf.RenderLoadedPageToBitmap(0, 96);
try
Assert((Before.Width = After.Width) and (Before.Height = After.Height));
// اینجا پیکسلهای نشانگر گوشه را مقایسه کن، نه فقط ابعاد را
finally
After.Free;
end;
finally
Before.Free;
end;
برای پیشزمینهٔ بیشتر، سمت حاشیهنویسی این داستان در ساختن appearance حاشیهنویسی قبل از تخت کردنش ادامه پیدا میکند، رندرکنندهٔ پشت مقایسهٔ قبل-و-بعد در رندر کردن یک صفحهٔ PDF بارگذاریشده به بیتمپ پوشش داده شده، و redaction و دوخت N-up روی PDFهای بارگذاریشده همان تکنیک اضافه کردن به content stream را نشان میدهد که پیشوند و پسوند چرخش به آن تکیه دارند. HotPDF، از جمله FlattenLoadedPageRotation و خوانندههای page-box، برای Delphi و C++Builder در صفحهٔ HotPDF Delphi PDF component در دسترس است