مقاله فنی

ترسیم Canvas در HotPDF برای Delphi: مسیرهای برداری و رنگ

HotPDF گرافیک برداری را با ساختن یک مسیر روی صفحه جاری و سپس درخواست برای رنگ آمیزی آن رسم می کند. میان این دو هیچ مرحله bitmap وجود ندارد. خطی که با MoveTo و LineTo می کشید در جریان محتوای PDF به عملگرهای مسیر تبدیل می شود، بنابراین واقعاً برداری می ماند: در بزرگنمایی 50% واضح است، در 1600% هم واضح است، و حجم آن فقط بخشی از چیزی است که نسخه rasterized هزینه داشت. برای نمودارها، خطوط جدول، محورهای چارت و تزیینات فرم، این دقیقاً همان چیزی است که می خواهید، و API پشت آن هم آن قدر کوچک است که می شود در یک نشست یادش گرفت

تمام سطح رسم روی THotPDF.CurrentPage قرار دارد. بین BeginDoc و EndDoc رنگ و عرض خط را روی همان شیء صفحه تنظیم می کنید، هندسه را می گذارید، و یک عملگر رنگ آمیزی را صدا می زنید تا نتیجه ثبت شود. چهار primitive ای که بیشتر از همه استفاده می کنید عبارت اند از MoveTo و LineTo برای مسیرهای دلخواه، Rectangle برای جعبه ها، Circle برای دیسک ها، و دو عملگر رنگ آمیزی Stroke و Fill

سیستم مختصات از پایین چپ شروع می شود

این همان نکته ای است که تقریباً همه کسانی را که از VCL می آیند گیر می اندازد. TCanvas که با آن کنترل ها را رسم می کنید، مبدأ را در گوشه بالا چپ می گذارد و Y به سمت پایین رشد می کند. PDF برعکس عمل می کند. HotPDF از گوشه پایین چپ صفحه بر حسب point یعنی 1/72 اینچ اندازه می گیرد و Y هرچه به بالا می روید بیشتر می شود. نقطه ای با Y := 720 نزدیک بالای صفحه US Letter قرار می گیرد که 792 point ارتفاع دارد، و Y := 50 نزدیک پایین قرار می گیرد. اگر اولین رسم شما عمودی آینه شود، دلیلش همین است: کدی که از گرافیک صفحه نمایش منتقل شده جهت نادرست را فرض کرده و از لبه پایین بیرون زده است

همین قرارداد برای TextOut هم برقرار است، بنابراین وقتی آن را در ذهن جا بیندازید، متن و شکل ها هر دو از یک مدل ذهنی پیروی می کنند. چیدمان را با تصمیم گرفتن درباره محل کف هر عنصر برنامه ریزی کنید، نه بالای آن، و بقیه خودبه خود دنبال می شود

مسیرها: MoveTo، LineTo، Stroke

یک مسیر stroked مثل این است که قلم را بردارید، بگذارید و بکشید. MoveTo قلم را بلند می کند و نقطه شروع را بدون گذاشتن اثری تعیین می کند. هر LineTo مسیر جاری را تا نقطه ای تازه ادامه می دهد. تا وقتی Stroke را صدا نزنید چیزی روی صفحه ظاهر نمی شود. Stroke مسیر جمع شده را با رنگ stroke و عرض خط فعلی رسم می کند و سپس مسیر را پاک می کند تا MoveTo بعدی از نو شروع شود

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

دو جزئیات واقعاً در زمان عیب یابی صرفه جویی می کند. عرض خط یک state است، نه یک آرگومان: SetLineWidth آن را یک بار تعیین می کند و هر Stroke بعدی تا وقتی دوباره عوضش نکنید از همان مقدار استفاده می کند، به همین دلیل polyline بالا از آن خط افقی ضخیم تر است. و مسیر بعد از هر Stroke ریست می شود، بنابراین اگر Stroke را فراموش کنید، هندسه ای که با دقت چیده بودید اصلاً render نمی شود. اگر شکلی در خروجی نیست، اولین جایی که باید نگاه کنید همان فراخوانی رنگ آمیزی است

مختصات بر حسب point هستند، و point می تواند کسری باشد. MoveTo و LineTo مقادیر Single می پذیرند، بنابراین خطی به نازکی 0.5 point یا موقعیتی مثل 72.25 هم مجاز و معنادار است و به نزدیک ترین عدد صحیح گرد نمی شود. این دقت از دو جهت مخالف اهمیت دارد. عرض خط کمتر از حدود 0.5 ممکن است به باریک ترین خط ممکن وابسته به دستگاه تبدیل شود که روی صفحه ناپدید می شود و در چاپ دوباره ظاهر می شود، بنابراین برای یک rule قابل مشاهده بهتر است عرض را عمداً تعیین کنید نه اینکه به پیش فرض تکیه کنید. در طرف دیگر، اگر خطوط جدول و gridline ها را روی مختصات point کامل بچسبانید، شبکه های متراکم به خاطر گرد شدن متفاوت خطوط مجاور کمی ناهموار به نظر نمی رسند. اگر فاصله grid را از اول بر حسب point تعیین کنید، بقیه چیدمان هم از آن پیروی می کند

شکل های پر شده و رنگ

primitive های بسته را می توان به جای خط دور، پر کرد. Rectangle یک موقعیت و یک اندازه می گیرد، Circle یک مرکز و یک شعاع می گیرد، و هر دو با Fill در رنگ fill فعلی یا با Stroke فقط به صورت outline ثبت می شوند. رنگ fill و رنگ stroke دو state جدا هستند و با SetRGBFillColor و SetRGBStrokeColor تعیین می شوند که هر دو یک TColor می گیرند. این یعنی می توانید مستقیم از ثابت های رنگ Delphi و helperِ RGB استفاده کنید

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

به شکل آرگومان های Rectangle دقت کنید. این تابع از فرم موقعیت به علاوه اندازه استفاده می کند، یعنی X, Y, Width, Height، نه دو گوشه مقابل. TCanvas.Rectangle که برنامه نویس Delphi به آن عادت دارد از (Left, Top, Right, Bottom) استفاده می کند، بنابراین حافظه عضلانی ممکن است گوشه دوم را جایی بدهد که HotPDF انتظار عرض و ارتفاع دارد و در نتیجه جعبه با اندازه اشتباه در می آید. جفت (X, Y) گوشه پایین چپ است، مطابق با مبدأ صفحه. برای دایره، (X, Y) مرکز است و آرگومان سوم شعاع بر حسب point است

یک انتخاب رنگ که نمونه قدیمی اشتباه انجام می داد

نسخه ای قدیمی از این مثال برای هر شکل از Random($FFFFFF) استفاده می کرد. ظاهرش زنده است، اما برای اسناد تولیدی انتخاب درستی نیست. PDF ای که با کد می سازید معمولاً چیزی است که بعداً می خواهید آن را هم test کنید، و رنگ های fill تصادفی مقایسه خروجی را بین اجراها غیرممکن می کنند: diff بایت به بایت با یک فایل سالم مرجع هر بار بدون دلیل واقعی شکست می خورد. رنگ های صریح انتخاب کنید. اگر تنوع میان چند شکل می خواهید، آن را از داده یا یک آرایه palette ثابت بگیرید تا همان ورودی همیشه همان فایل را بسازد. وقتی یک artifact از خط انتشار عبور می کند، determinism از تازگی مهم تر است

ترکیب primitive ها: یک کادر callout

هر primitive به تنهایی ساده است؛ ارزش واقعی وقتی دیده می شود که چندتای آن ها با هم چیزی بسازند که یک گزارش واقعاً به آن نیاز دارد. یک callout، همان جعبه توضیحی که به یک شکل اشاره می کند و آن را توضیح می دهد، از همه چیزهایی که تا اینجا گفتیم استفاده می کند: یک مستطیل پرشده با border، یک خط pointer با stroke، یک نقطه برای لنگر کردن pointer، و متنی که داخل جعبه با همان مختصات پایین چپ چیده شده است. FillAndStroke اینجا دقیقاً به درد می خورد، چون interior و outline را در یک commit برای یک مسیر رنگ آمیزی می کند و لازم نیست rectangle را دوباره بسازید

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

دقت کنید این ترکیب به چه مدیریت state کمی نیاز دارد. رنگ fill، رنگ stroke و عرض خط هر کدام دقیقاً پیش از شکلی که به آن ها نیاز دارد تنظیم می شوند، بنابراین هر بخش از رسم مثل یک واحد مستقل خوانده می شود و می توان آن را جابه جا یا به یک helper منتقل کرد بدون اینکه state پنهان را با خود بکشد. اگر این را درون یک procedure با گرفتن نقطه لنگر و رشته ها بپیچید، با هزینه حدود چهل خط یک annotation قابل استفاده مجدد برای نمودار خواهید داشت

کجا ترسیم برداری سود می دهد و کجا نه

وقتی هندسه تولیدی است، سراغ این فراخوانی های مسیر و شکل بروید: gridline ها و میله های نمودار، خطوط جدول فاکتور، callout box ها روی نمودار، یا یک نشان logo که با چند مسیر بیان شده است. همه این ها بدون blur مقیاس می شوند و تقریباً چیزی به حجم فایل اضافه نمی کنند، چون یک rectangle فقط چند عدد است نه هزاران pixel. طرف دیگر ماجرا هم روشن است. اگر چیزی که واقعاً دارید یک عکس یا screenshot است، آن را با AddImage و ShowImage به شکل تصویر رسم کنید؛ دنبال کردن یک bitmap با فراخوانی های برداری هیچ سودی ندارد. خط های مستقیم، rectangle ها و circle های بالا بخش بزرگی از کار واقعی گزارش سازی را پوشش می دهند، و سه refinement بعدی که برنامه نویس ها معمولاً درباره اش می پرسند، یعنی curve ها، الگوهای dash و transparency، همگی روی همین شیء صفحه قرار دارند

خلاصه ای کوتاه از curve، dash و transparency

curve های آزاد ادامه همان سازوکار مسیر هستند که همین حالا دارید. CurveToC(X1, Y1, X2, Y2, X3, Y3) از نقطه جاری یک قطعه cubic Bezier تا (X3, Y3) اضافه می کند که به سمت دو control point خم می شود، و شکل های کوتاه تر CurveToV و CurveToY حالت هایی را پوشش می دهند که یکی از control point ها با یک endpoint منطبق است. یک مسیر می تواند آزادانه قطعه های LineTo و CurveToC را پیش از یک Stroke یا Fill نهایی با هم ترکیب کند، و به همین ترتیب گوشه های گرد و خطوط نرم نمودار ساخته می شوند

stroke های dashed هم state هستند، دقیقاً مثل عرض خط. SetDash([3, 3], 0) هر stroke بعدی را به الگویی سه point روشن و سه point خاموش تغییر می دهد، جایی که آرایه طول run های روشن و خاموش را بر حسب point مشخص می کند و آرگومان دوم phase آغاز چرخه را تعیین می کند. NoDash قلم را به خط پیوسته برمی گرداند. آن را تنظیم کنید، gridline هایی را که می خواهند با آن رسم کنید، و پیش از rule بعدی که باید توپر باشد آن را reset کنید، وگرنه dash بی صدا به همه چیز بعدی سرایت می کند

transparency از طریق یک graphics state نام گذاری شده اعمال می شود، نه از طریق آرگومان رنگ، چون alpha در PDF جزئی از graphics state dictionary است. یکی از آن ها را روی سند با RegisterExtGState ثبت کنید و یک fill alpha و stroke alpha بین 0 و 1 بدهید، سپس نامی را که برمی گرداند با CurrentPage.SetGraphicsState اعمال کنید؛ از آن لحظه به بعد fill و stroke با opacity ثبت شده رنگ آمیزی می شوند. این تشریفات از setter های رنگ سنگین تر است، اما همان بار اولی که یک highlight bar باید روی متن بنشیند و آن را پنهان نکند ارزشش روشن می شود

آخرین عادتی که ارزش حفظ کردن دارد verification است. هندسه تولیدی ممکن است روی ماشین شما درست باشد و روی ماشین مشتری خراب شود، معمولاً به خاطر font substitution در متنی که با آن مخلوط کرده اید یا فرضی درباره اندازه صفحه که در محیط مشتری برقرار نیست. فایل نهایی را در چند سطح zoom باز کنید تا مطمئن شوید لبه ها تمیز می مانند، و بررسی کنید که هر شکل واقعاً داخل جعبه margin مورد نظر شما می نشیند. اگر طرح رنگ deterministic داشته باشید، این بررسی را می توان به جای نگاه چشمی در برابر یک PDF مرجع خودکار کرد

فراخوانی های MoveTo، LineTo، Stroke، Fill و رنگ که در اینجا دیدید بخشی از HotPDF Component برای Delphi و C++Builder هستند