مقاله فنی

گرافیک PDF چگونه کار می‌کند: جریان‌های محتوا و اپراتورها

یک صفحه PDF پیکسل‌ها را ذخیره نمی‌کند، و درختی از اشیاء شکل (shape objects) را به آن روشی که SVG انجام می‌دهد ذخیره نمی‌سازد. یک برنامه را ذخیره می‌کند. هر خط، منحنی، پُر کردن (fill)، و تصویر قرار داده شده روی صفحه، نتیجه اجرای دنباله‌ای از اپراتورها در یک جریان محتوا (content stream)، از بالا به پایین، در برابر یک وضعیت گرافیکیِ در حال اجرا است. این یک واقعیت را درک کنید و اکثر رفتارهای فرمت دیگر تعجب‌آور نخواهد بود: چرا یک fill به یک اپراتور نقاشی جداگانه پس از ساخته شدن مسیر نیاز دارد، چرا رنگ‌ها و عرض خطوط از یک شکل به شکل بعدی نشت می‌کنند مگر اینکه آن‌ها را در براکت قرار دهید، چرا یک کد ترسیم یکسان می‌تواند پس از یک تبدیل مختصات (coordinate transform) واحد در مکان‌های کاملاً متفاوتی فرود آید. این گشتی در آن مدل اجرایی است همان‌طور که در ISO 32000 تعریف شده است: اپراتورهایی که با آن‌ها ملاقات می‌کنید وقتی یک جریان محتوا را باز می‌نمایید، و قوانینی که تصمیم می‌گیرند چه چیزی روی صفحه ظاهر شود

جریان محتوا، بایت‌کدِ پسوندی است

یک جریان محتوا توالی مسطحی از بایت‌ها از عملوندها (operands) و به دنبال آن اپراتورها (operators) است. عملوندها اول می‌آیند، اپراتوری که آن‌ها را مصرف می‌کند در آخر می‌آید، که برعکس یک فراخوانی تابع (function call) و دقیقاً مشابه یک ماشین پشته (stack machine) است: اعداد را پوش (push) کنید، سپس فعل را صادر نمایید. هیچ تودرتویی (nesting)، هیچ گرامر عبارت (expression syntax)، و هیچ متغیری وجود ندارد. طرح‌نمای یک مثلث، پنج خط از این است:

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

اپراتورها به صورت هدفمندی موجز (terse) هستند. یک صفحه واقعی هزاران مورد از این‌هاست که معمولاً با FlateDecode فشرده شده‌اند. هزینه آن فشردگی این است که جریان هیچ ساختاری که بتوانید از آن کوئری بگیرید با خود حمل نمی‌کند: یک نمایشگر نمی‌تواند بپرسد "تیتر در این صفحه کجاست"، تنها می‌تواند برنامه را اجرا کند و ببیند چه جوهری در کجا فرود می‌آید. این دلیل ریشه‌ای است که استخراج متن از PDFهای دلخواه سخت است

مبدأ پایین-چپ است، و Y به سمت بالا رشد می‌کند

قبل از اینکه هر مختصاتی منطقی باشد باید بدانید (0, 0) کجاست. PDF مبدأ (origin) را در گوشه پایین-چپ صفحه قرار می‌دهد، به طوری که X به سمت راست و Y به سمت بالا افزایش می‌یابد، و بر حسب نقطه با 72 نقطه در اینچ اندازه‌گیری می‌شود (ISO 32000-2 §8.3.2). در یک صفحه US Letter لبه بالایی در y = 792 قرار دارد، نه در y = 0. هر کسی که از گرافیک صفحه نمایش می‌آید، جایی که مبدأ بالا-چپ است و Y به سمت پایین رشد می‌کند، در اولین تلاش این را برعکس متوجه می‌شود و خط اول را خارج از پایین صفحه رسم می‌نماید. این واحد همچنین از رسانه (medium) مستقل است: 72 واحد برابر است با یک اینچ، چه صفحه روی صفحه نمایش تلفن رندر شود و چه روی یک دستگاه چاپ تصویر (imagesetter)

بیشتر کتابخانه‌های ترسیم صفحه این قرارداد را به طور مستقیم به ارث می‌برند. به عنوان مثال، در HotPDF، متد TextOut و فراخوانی‌های مسیر همگی از پایین-چپ بر حسب نقطه اندازه‌گیری می‌کنند، بنابراین یک مقدار نزدیک به ارتفاع صفحه، محتوا را در بالا قرار می‌دهد:

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

آن توالی فراخوانی دقیقاً به اپراتورهای m، l و S بالا کامپایل می‌شود. کتابخانه یک ماشین‌نویس (typist) برای جریان محتوا است، نه چیزی بیشتر، و دانستن اینکه چه چیزی منتشر می‌کند همان چیزی است که به شما اجازه می‌دهد درباره خروجی استدلال کنید وقتی شکلی در جایی که انتظار نداشتید فرود می‌آید

مسیر را بسازید، سپس آن را رنگ‌آمیزی کنید

PDF ساختن مسیر را از نقاشی مسیر (path painting) جدا می‌سازد، و این جداسازی فضل‌فروشی (pedantry) نیست. شما ابتدا شکلی را با اپراتورهای ساخت توصیف می‌کنید که هیچ‌چیز قابل‌رویتی اضافه نمی‌کنند، سپس یک اپراتور نقاشی صادر می‌نمایید که تصمیم می‌گیرد با مسیر انباشته‌شده چه کار کند. همان مثلث می‌تواند یک طرح کلی، یک پُر کردن (fill) یکدست، یا هر دو باشد، که فقط به فعلی که با آن پایان می‌دهید بستگی دارد

اپراتورهای ساخت کم هستند. m یک زیرمسیر جدید را در یک نقطه شروع می‌کند. l یک بخش مستقیم اضافه می‌نماید. c یک منحنی Bezier مکعبی از شش عملوند، یعنی دو نقطه کنترل و یک نقطه پایان اضافه می‌کند. re یک میانبر است که یک مستطیل کامل را از یک چهارتایی x، y، عرض و ارتفاع اضافه می‌سازد. h زیرمسیر فعلی را به ابتدای آن می‌بندد. هیچ‌کدام از آن‌ها جوهری روی صفحه قرار نمی‌دهند؛ آن‌ها فقط هندسه انباشت می‌کنند

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

مثال اصلی از متغیر منسوخ y اپراتور منحنی استفاده می‌کرد؛ c با سه نقطه صریح آن فرمی است که در عمل خواهید دید و باید به دنبال آن باشید. هنگامی که مسیر وجود داشته باشد، یک اپراتور نقاشی آن را تمام می‌کند. دایره لغات کوچک است و ارزش حفظ کردن را دارد، زیرا هر شکلی در هر صفحه‌ای با یکی از این موارد پایان می‌یابد:

  • اپراتور S طرح کلی مسیر را با استفاده از عرض خط فعلی و رنگ خط (stroke color) رسم می‌کند
  • اپراتور f فضای داخلی را با استفاده از رنگ پرکردن فعلی و قانون پیچش غیرصفر (nonzero winding rule) پر می‌سازد
  • اپراتور f* با استفاده از قانون زوج-فرد (even-odd rule) پر می‌نماید، که برای اشکالی که خود را قطع می‌کنند و اشکال سوراخ‌دار اهمیت دارد
  • اپراتور B در یک عملیات هم پر می‌کند و هم رسم می‌نماید؛ b ابتدا مسیر را می‌بندد
  • اپراتور n هیچ چیزی رنگ‌آمیزی نمی‌کند، که اینگونه است که یک مسیر بدون باقی گذاشتن یک علامت قابل‌رویت تبدیل به یک منطقه برش (clip region) می‌شود

قانون پیچش (winding rule) بخشی است که افراد اشتباه متوجه می‌شوند. حالت غیرصفر (f, B) تقاطع‌های علامت‌دار یک پرتو از نقطه آزمایش را می‌شمارد و هر جا که تعداد صفر نباشد را پر می‌نماید، بنابراین یک سوراخ تنها در صورتی خالی می‌ماند که زیرمسیر آن در جهت مخالف با زیرمسیر بیرونی پیچیده شده باشد. حالت زوج-فرد (f*, B*) در هر تقاطع بدون توجه به جهت تغییر وضعیت (toggle) می‌دهد. اگر یک شکل "دونات" توپر بیرون بیاید، دایره داخلی به همان روش دایره بیرونی پیچیده شده است، و شما یا باید آن را معکوس کنید یا به زوج-فرد تغییر دهید

رنگ یک حالت است، نه یک پارامتر

رنگ در یک جریان محتوا چسبناک (sticky) است. شما یک رنگ تعیین می‌کنید و تا زمانی که رنگ دیگری را تعیین نمایید یا یک وضعیت قبلی را بازیابی کنید، همان‌طور باقی می‌ماند، به همین دلیل است که یک تغییر رنگ بدون براکت به طور بی‌صدا به همه چیزهایی که بعد از آن رسم می‌شوند رنگ (tint) می‌زند. PDF همچنین رنگ پرکردن و رنگ خط را به عنوان دو تنظیم مستقل نگه می‌دارد، با حروف کوچک برای پرکردن و حروف بزرگ برای خط. هر کدام از فضاهای رنگی دستگاه (device color spaces) خلاصه‌نویسی خاص خود را دارند:

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

DeviceRGB مناسب خروجی صفحه نمایش است، DeviceCMYK چیزی است که تولیدات چاپی انتظار دارند، و DeviceGray کوچکترین انتخاب برای محتوای تک‌رنگ است. فضاهای دستگاه مناسب هستند اما کالیبره نشده‌اند: یک سه‌گانه RGB می‌تواند در دو مانیتور متفاوت رندر شود، که مشکلی است که فضاهای رنگی مبتنی بر ICC و اهداف خروجی (output intents) PDF/A برای حل آن وجود دارند. برای کارهای حساس به رنگ، شما یک فضای کالیبره‌شده را با cs و CS انتخاب می‌کنید و اجزا را با sc و scn تنظیم می‌نمایید، اما برای اسناد معمولی خلاصه‌نویسی‌های دستگاه بار را به دوش می‌کشند. یک کتابخانه این موارد را در فراخوانی‌های نوع‌دار می‌پیچد. برای مثال، HotPDF یک TColor منفرد را می‌گیرد و اپراتورهای منطبق را منتشر می‌کند:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

وضعیت گرافیکی و پشته q/Q

هر چیزی که خود مسیر نیست در وضعیت گرافیکی (graphics state) زندگی می‌کند: ماتریس تبدیل فعلی، رنگ‌های پرکردن و خط، عرض خط، الگوی خط‌چین (dash pattern)، منطقه برش (clip region)، آلفا. وضعیت عمومی و قابل تغییر (mutable) است، بنابراین تنها راه امن برای ایجاد یک تغییر محلی این است که کل چیز را ذخیره کنید، آن را تغییر دهید، رسم نمایید، و آن را برگردانید (roll back). این کاری است که q و Q انجام می‌دهند. اپراتور q یک کپی از وضعیت فعلی را روی یک پشته پوش می‌کند؛ Q آن را پاپ (pop) می‌سازد، و هر تغییری را که از زمان q منطبق ایجاد شده است دور می‌اندازد

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

q و Q نامتعادل، روش رایجی است که یک جریان محتوای دست‌ساز یا دوخته‌شده (stitched) به خطا می‌رود. یک q سرگردان بدون هیچ Q منطبقی باعث می‌شود وقتی صفحه به پایان می‌رسد پشته عمیق بماند؛ یک Q اضافی باعث سرریز (underflow) آن می‌گردد. در هر صورت یک نمایشگر ممکن است یک برش یا تبدیل قدیمی را در حال اجرا نگه دارد، و محتوا ناپدید شود یا در مکان اشتباهی فرود آید. وقتی گرافیک بدون هیچ دلیلی که مسیر بتواند توضیح دهد ناپدید می‌شود، ابتدا پشته وضعیت را ممیزی (audit) کنید

ماتریس CTM هر مختصاتی را تبدیل می‌کند

ماتریس تبدیل فعلی (CTM - current transformation matrix) بین اعداد در اپراتورهای شما و صفحه واقعی قرار می‌گیرد. هر مختصاتی قبل از اینکه چیزی رسم شود در CTM ضرب می‌شود، بنابراین تغییر ماتریس، مکان و نحوه نمایش تمام ترسیم‌های بعدی را بدون دست زدن به یک مختصات مسیر تغییر می‌دهد. اپراتور cm یک ماتریس جدید را روی ماتریس فعلی الحاق (concatenate) می‌کند، و شش عملوند را می‌گیرد که به ماتریس آفین (affine matrix) [a b c d e f] نگاشت می‌شوند:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

دو چیز افراد را دچار اشتباه می‌سازد. اول، cm به جای جایگزینی، ترکیب (compose) می‌شود، بنابراین تبدیل‌ها انباشته می‌گردند و ترتیب اهمیت دارد: مقیاس‌دهی (scaling) و سپس انتقال (translating) با انتقال و سپس مقیاس‌دهی یکسان نیست. دوم، چرخش (rotation) و مقیاس‌دهی حول مبدأ فعلی می‌چرخند، نه مرکز شکل شما، بنابراین برای چرخاندن چیزی در جای خود، آن را به مبدأ منتقل می‌کنید، می‌چرخانید، سپس به عقب منتقل می‌نمایید، که همگی در q/Q پیچیده شده‌اند. همین ماتریس است که تصاویر را در جای خود قرار می‌دهد، یعنی آخرین قطعه‌ای که ارزش دیدن دارد

تصاویر و محتوای قابل استفاده مجدد XObject هستند

تصاویر رستر (Raster) به صورت درون‌خطی در جریان محتوا زندگی نمی‌کنند. آن‌ها به عنوان XObjectهای تصویر ذخیره می‌شوند، یعنی اشیاء خارجی با دیکشنری خاص خود که عرض، ارتفاع، عمق بیت، فضای رنگی و فیلتر فشرده‌سازی را توصیف می‌کند، و جریان محتوا تنها به آن‌ها ارجاع می‌دهد. یک عکس با پشتیبان JPEG خود را به این صورت اعلان می‌نماید:

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

یک XObject تصویر در مربع واحد رسم می‌شود: آن همیشه ناحیه (0, 0) تا (1, 1) را در فضای کاربر اشغال می‌کند. شما موقعیت یا اندازه‌ای را به آن ارسال نمی‌نمایید. در عوض شما CTM را طوری تنظیم می‌کنید که آن مربع واحد به مستطیلی که می‌خواهید نگاشت شود، سپس آن را با Do فراخوانی می‌کنید. به همین دلیل است که قرار دادن یک تصویر همیشه یک تبدیل به دنبال یک فراخوانی است، که در یک save/restore پیچیده شده است تا مقیاس به عملیات بعدی نشت (bleed) نکند:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

همان مکانیسم Do، به XObjectهای فرم که تکه‌ای از گرافیک قابل استفاده مجدد، یک لوگو یا یک مهر تکراری را به عنوان جریان محتوای خاص خود با یک جعبه مرزی (bounding box) نگه می‌دارند، حرکت می‌دهد. آن را یک بار تعریف کنید، بارها با یک CTM متفاوت فراخوانی نمایید، و بایت‌ها تنها یک بار در فایل ظاهر می‌گردند. بیشتر کتابخانه‌ها این کار را در پشت یک فراخوانیِ قراردهی (placement) پنهان می‌سازند: HotPDF یک بیت‌مپ را با AddImage ثبت می‌نماید و آن را با ShowImage در جای خود قرار می‌دهد، و به جای اینکه از شما بخواهد ماتریس را با دست بسازید یک x، y، عرض و ارتفاع صریح دریافت می‌کند:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

در زیر آن یک خط، کتابخانه دیکشنری XObject تصویر را می‌نویسد، CTM را طوری تنظیم می‌سازد تا اندازه و موقعیت مربع واحد را تعیین کند، و Do را منتشر می‌نماید. مدل زیرین همان چیزی است که ارزش دانستن را دارد، زیرا هر نتیجه عجیبی را توضیح می‌دهد: یک تصویر کشیده شده (stretched) یک CTM با فاکتورهای مقیاس ناهماهنگ است، یک لوگوی یکسان در چهل صفحه در واقع یک XObject فرم است که چهل بار فراخوانی شده است، و تصویری که وارونه رندر می‌شود در واقع یک چرخش علامت در ماتریس است، نه یک فایل خراب

این موضوع به کجا می‌رسد

مدل گرافیکی زمانی که شکل آن را ببینید کوچک است. یک جریان محتوا، بایت‌کد پسوندی است که در برابر یک وضعیت قابل تغییر اجرا می‌گردد؛ مختصات در پایین-چپ شروع می‌شوند و از CTM عبور می‌کنند؛ مسیرها به طور بی‌صدا ساخته شده و با یک اپراتور سنجیده نقاشی می‌شوند؛ تنظیمات رنگ و خط باقی می‌مانند تا زمانی که آن‌ها را با q/Q براکت‌بندی کنید؛ تصاویر و گرافیک‌های قابل استفاده مجدد XObjectهایی هستند که با تبدیل یک مربع واحد در جای خود قرار می‌گیرند. تقریباً هر نتیجه رندرینگ گیج‌کننده‌ای به یکی از آن پنج قانون تقلیل می‌یابد. اگر می‌خواهید ببینید این اپراتورهای گرافیکی چگونه در داخل مدل شیء بزرگتر قرار می‌گیرند، دیکشنری‌های صفحه و جدول ارجاع متقاطع که به آن‌ها اشاره می‌نمایند، مقاله بررسی فنی ساختار فایل PDF آن لایه را پوشش می‌دهد، و مقاله ساخت یک سند ساده PDF از ابتدا بایت‌ها را از ابتدا تا انتها طی می‌کند. ترسیم متن در خانواده اپراتورهای خاص خود زندگی می‌نماید و دام‌های خاص خود را دارد، که در مقاله همراه در زمینه مدیریت متن و فونت PDF پوشش داده شده است

فراخوانی‌های ترسیم Delphi که در اینجا نشان داده شده است، یعنی MoveTo، LineTo، Stroke، Rectangle، Fill، SetRGBFillColor، AddImage، و ShowImage، بخشی از ابزار HotPDF Component برای Delphi و C++Builder هستند، که این اپراتورهای جریان محتوا را برای شما منتشر می‌کند