Technical Article

گرافیک برداری در PDF با Delphi: مسیرها و گرادینت‌ها

بسیاری از کدهای Delphi که با PDF کار می‌کنند، این فرمت را به عنوان ظرفی برای دو چیز در نظر می‌گیرند: بخش‌هایی از متن و چند بیت‌مپ قرار داده شده. این دیدگاه تا جایی که پیش می‌رود درست است و تواناترین بخش فرمت را بلااستفاده می‌گذارد. یک صفحه PDF یک بوم دوبعدی مستقل از رزولوشن است که بر اساس همان مدل تصویربرداری PostScript ساخته شده است. این صفحه می‌تواند خطوط، منحنی‌ها، نواحی پر شده، گرادینت‌ها و الگوهای تکرارشونده را ترسیم کند، همه به عنوان بردارهایی که در هر زوم تیز می‌مانند و با رزولوشن کامل دستگاه چاپ می‌شوند. اگر در حال طراحی یک لوگو، نمودار، واترمارک یا حاشیه گواهی هستید، مسیر برداری تقریباً همیشه عنصر اصلی مناسبی است و نسبت به تصویر رسترشده‌ای که بسیاری از برنامه‌ها به جای آن استفاده می‌کنند، کوچک‌تر و واضح‌تر است.

این مقاله مدل برداری را همان‌طور که در ISO 32000-1 تعریف شده است بررسی می‌کند و فراخوانی‌های منطبق با PDFlibPas را نشان می‌دهد. هدف، ملموس کردن مشخصات فنی است، زیرا API از نزدیک بر روی آن منطبق است و درک یکی، دیگری را به شما آموزش می‌دهد.

صفحه یک ماشین مسیر است

بخش ۸.۵ استاندارد ISO 32000-1 گرافیک را در دو فاز توصیف می‌کند که هرگز همپوشانی ندارند. ابتدا یک مسیر را می‌سازید که هندسه محض بدون هیچ نتیجه بصری است. سپس آن مسیر را در یک عملیات واحد رنگ‌آمیزی می‌کنید که دور آن را خط می‌کشد (stroke)، داخلش را پر می‌کند یا هر دو کار را انجام می‌دهد. هیچ‌چیز در طول ساخت روی صفحه ظاهر نمی‌شود. مسیر یک دنباله انتزاعی از نقاط و بخش‌ها است که در حالت گرافیکی (graphics state) نگه داشته می‌شود تا زمانی که یک اپراتور رنگ‌آمیزی آن را مصرف کند، در این مرحله رندر شده و دور انداخته می‌شود.

یک مسیر از یک یا چند زیرمسیر (subpath) ساخته شده است. یک زیرمسیر در یک نقطه شروع می‌شود و با افزودن بخش‌ها رشد می‌کند: خطوط مستقیم، منحنی‌های مکعبی Bezier و در برخی پلتفرم‌ها مستطیل‌های کاملی که به عنوان زیرمسیر بسته خودشان اضافه می‌شوند. در PDFlibPas شما یک مسیر را با StartPath باز می‌کنید که نقطه شروع را تعیین می‌کند، سپس آن را با AddLineToPath و AddCurveToPath گسترش می‌دهید. هر فراخوانی یک نقطه فعلی ضمنی را به جلو می‌برد، بنابراین بخش بعدی از جایی که بخش قبلی به پایان رسیده ادامه می‌یابد. ClosePath یک بخش مستقیم نهایی را به شروع زیرمسیر ترسیم می‌کند که این موضوع برای خط کشیدن (stroking) مهم است زیرا باعث ایجاد یک اتصال خط واقعی در راس بسته به جای دو کلاهک انتهایی شل می‌شود.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

منحنی‌ها از AddCurveToPath استفاده می‌کنند که دو نقطه کنترل Bezier و یک نقطه پایان را می‌گیرد: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). منحنی از نقطه فعلی به (EndX, EndY) اجرا می‌شود و در این مسیر به سمت دو نقطه کنترل کشیده می‌شود. کمان‌های دایره‌ای از طریق AddArcToPath(CenterX, CenterY, TotalAngle) در دسترس هستند، جایی که شعاع از فاصله بین نقطه فعلی و مرکز گرفته می‌شود و موتور کمان را به عنوان زنجیره‌ای از بخش‌های Bezier صادر می‌کند. مستطیل‌ها دارای یک میانبر هستند، AddBoxToPath(Left, Top, Width, Height)، که یک مستطیل بسته کامل را به عنوان زیرمسیر خود بدون نیاز به StartPath قبلی اضافه می‌کند.

دو قانون پر کردن، و چرا با هم اختلاف دارند

وقتی مسیری را پر می‌کنید که خودش را قطع می‌کند یا حاوی یک حلقه داخلی است، رندرر به قانونی برای تصمیم‌گیری در مورد اینکه کدام مناطق داخل شکل هستند و کدام سوراخ هستند نیاز دارد. بخش ۸.۵.۳.۳ استاندارد ISO 32000-1 دو قانون را تعریف می‌کند و آن‌ها می‌توانند هندسه یکسانی را به شکل متفاوتی رنگ‌آمیزی کنند. قانون پیچش غیرصفر (nonzero winding rule) تقاطع‌های علامت‌دار پرتوی تابیده‌شده از نقطه تست به بی‌نهایت را می‌شمارد، برای هر بخشی که از چپ به راست قطع می‌کند یکی اضافه و برای هر کدام که برعکس قطع می‌کند یکی کم می‌کند؛ نقطه زمانی داخل است که مجموع صفر نباشد. قانون زوج-فرد (even-odd rule) جهت را نادیده می‌گیرد و به سادگی تقاطع‌ها را می‌شمارد و در صورتی که تعداد فرد باشد، نقطه را داخل می‌نامد.

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

مجموعه PDFlibPas قانون را بر اساس فراخوانی که برای رنگ‌آمیزی انجام می‌دهید انتخاب می‌کند، نه با یک پرچم. DrawPath با قانون پیچش غیرصفر پر می‌کند; DrawPathEvenOdd با قانون زوج-فرد پر می‌کند. هر دو حالت عدد صحیح یکسانی را می‌گیرند: 0 فقط دور خط را رسم می‌کند، 1 فقط پر می‌کند و 2 پر می‌کند و دور خط را می‌کشد. قانون زوج-فرد ابزار آسان‌تری برای ایجاد سوراخ‌ها است دقیقاً به این دلیل که نیازی ندارد جهت زیرمسیر را مدیریت کنید.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

گرادینت‌های محوری رنگ را در امتداد یک خط تغییر می‌دهند

یک رنگ پرکننده تخت یک مقدار واحد در کل ناحیه است. یک گرادینت رنگ را به طور مداوم تغییر می‌دهد و ساده‌ترین نوع آن گرادینت محوری (axial) یا خطی (linear) است. بخش ۸.۷.۴.۵ استاندارد ISO 32000-1 آن را به عنوان سایه‌روشن محوری نوع ۲ (Type 2 axial shading) مشخص می‌کند: شما دو نقطه را که یک محور را تعریف می‌کنند، یک رنگ شروع در نقطه اول و یک رنگ پایان در نقطه دوم می‌دهید و رندرر رنگ را در امتداد آن محور درون‌یابی می‌کند. هر نقطه در ناحیه پر شده، رنگ تصویر عمود خود روی محور را به خود می‌گیرد، بنابراین گرادینت در باندهایی با زوایای قائم نسبت به خط بین دو نقطه اجرا می‌شود.

در PDFlibPas یک گرادینت یک منبع سند نام‌گذاری شده است که شما یک بار ایجاد کرده و سپس به عنوان رنگ فعال انتخاب می‌کنید. NewRGBAxialShader آن را ثبت می‌کند. امضا به صورت NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend) است: دو نقطه انتهای محور، سه‌گانه‌های RGB در هر انتها به عنوان مقادیری در محدوده ۰ تا ۱ و یک پرچم Extend. با تنظیم Extend روی 1، رنگ‌های انتهایی به عنوان پرکننده توپر فراتر از نقاط انتهای محور ادامه می‌یابند، که این همان چیزی است که معمولاً می‌خواهید تا گوشه‌های ناحیه خارج از محور بدون رنگ رها نشوند؛ 0 آن‌ها را دست‌نخورده می‌گذارد. هنگامی که شیدر وجود دارد، آن را با SetFillShader برای نواحی پر شده، SetLineShader برای طرح‌های خط کشیده شده یا SetTextShader برای متن متصل می‌کنید. این اتصال برای فراخوانی‌های ترسیمی که به دنبال می‌آیند فعال می‌ماند، بنابراین مسیری که در مرحله بعد رنگ‌آمیزی می‌کنید به جای یک رنگ تخت، گرادینت را به خود می‌گیرد.

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

محور در اینجا عمودی است، از y=100 تا y=260 در یک x ثابت، بنابراین باندهای رنگی به صورت افقی اجرا می‌شوند و مستطیل از آبی در پایه خود به سفید در بالا محو می‌شود. از آنجا که شیدر با نام کلیدگذاری می‌شود، یک تعریف می‌تواند هر تعداد شکل را در صفحه پر کند و بازگشت به یک رنگ تخت فقط یک فراخوانی دیگر SetFillColor قبل از مسیر بعدی است.

الگوهای کاشی‌کاری یک سلول را تکرار می‌کنند

در حالی که یک گرادینت یک رنگ واحد را به آرامی تغییر می‌دهد، یک الگوی کاشی‌کاری (tiling pattern) یک اثر هنری کوچک را در یک منطقه تکرار می‌کند. بخش ۸.۷.۳.۱ استاندارد ISO 32000-1 الگوی کاشی‌کاری را به عنوان یک سلول الگو (pattern cell)، یک بخش مستقل از محتوا، تعریف می‌کند که رندرر آن را روی یک شبکه ثابت تکرار می‌کند تا ناحیه در حال رنگ‌آمیزی را کاشی‌کاری کند. به این ترتیب می‌توانید هاشور را برای یک پرکننده مهندسی، یک طرح تکراری برند در پشت یک سربرگ یا یک پس‌زمینه بافت‌دار بسازید که برداری و تیز می‌ماند و بدون توجه به وسعت ناحیه تقریباً هیچ حجمی ندارد، زیرا سلول یک بار ذخیره شده و در همه جا به آن ارجاع داده می‌شود.

مجموعه PDFlibPas سلول الگو را از محتوای صفحه ضبط‌شده می‌سازد. شما یک صفحه یا یک ناحیه را با CapturePage ضبط می‌کنید، ضبط را با NewTilingPatternFromCapturedPage(PatternName, CaptureID) به یک الگوی نام‌گذاری شده تبدیل می‌کنید و سپس آن الگو را با SetFillTilingPattern(PatternName) به عنوان پرکننده فعلی انتخاب می‌نمایید. از آن لحظه به بعد، هر مسیری که پر می‌کنید با سلول تکراری به جای یک رنگ تخت رنگ‌آمیزی می‌شود، دقیقاً همان‌طور که یک پرکننده شیدر کار می‌کند اما با یک سلول کاشی‌کاری شده به عنوان منبع رنگ. این توالی پیچیده‌تر از یک فراخوانی واحد است، بنابراین اگر مرحله ضبط ناآشنا است، الگو را به عنوان یک عملیات دو مرحله‌ای در نظر بگیرید: ابتدا سلول ضبط‌شده را تولید کنید، سپس قبل از ترسیم ناحیه‌ای که می‌خواهید کاشی‌کاری شود، آن را به عنوان یک پرکننده با نام متصل کنید.

در کنار هم قرار دادن عناصر اصلی

قطعات مستقیماً ترکیب می‌شوند. یک لکه Bezier پر شده، مسیری از منحنی‌ها است که با DrawPath رنگ‌آمیزی شده است. همان خط دوری که پس از افزودن یک حلقه داخلی با DrawPathEvenOdd رنگ‌آمیزی شده است، سوراخی را نشان می‌دهد که پرکننده پیچشی آن را می‌بست. یک مستطیل پر شده با گرادینت، جعبه‌ای است که به یک شیدر متصل شده است. مثال زیر هر سه را به ترتیب ترسیم می‌کند تا تفاوت بین دو قانون پر کردن در یک صفحه نمایان شود، سپس یک پنل گرادینت را در زیر آن‌ها قرار می‌دهد.

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

دو جزئیات ارزش حفظ کردن دارند. فراخوانی رنگ‌آمیزی قانون پر کردن را تعیین می‌کند، بنابراین انتخاب بین DrawPath و DrawPathEvenOdd انتخاب بین پیچش غیرصفر و زوج-فرد است و برای اشکال دارای سوراخ، قانون زوج-فرد شما را از استدلال در مورد جهت زیرمسیر بی‌نیاز می‌کند. و حالت گرافیکی در لحظه رنگ‌آمیزی نمونه‌برداری می‌شود: رنگ‌ها، عرض خط و اتصال شیدر خود را قبل از فراخوانی رنگ‌آمیزی تنظیم کنید، زیرا این همان حالتی است که موتور می‌خواند. ابتدا بسازید، حالت را پیکربندی کنید، در آخر رنگ‌آمیزی کنید و مدل برداری هر بار به طور قابل پیش‌بینی رفتار می‌کند.

از اینجا، گام‌های بعدی طبیعی خواندن مجدد بردارها و متن از یک سند موجود است که در مقاله ما در مورد استخراج متن، تصویر و فونت پوشش داده شده است، و رندر کردن همان مدل ترسیم به یک context دستگاه ویندوز برای پیش‌نمایش روی صفحه و چاپ، که در راهنمای پیش‌نمایش و چاپ پوشش داده شده است. فراخوانی‌های مسیر، شیدر و الگو توصیف‌شده در اینجا به عنوان بخشی از کتابخانه PDF Delphi در کنار APIهای متن، تصویر، فرم و امضا ارائه می‌شوند که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند.