Technical Article

Đồ họa vector trong PDF với Delphi: Đường dẫn và Gradient

Hầu hết mã Delphi chạm tới PDF đều coi định dạng này như một thùng chứa hai thứ: các đoạn văn bản và một vài bitmap được đặt vị trí. Quan điểm đó là đúng trong phạm vi của nó, nhưng nó để lại phần có khả năng nhất của định dạng không được sử dụng. Một trang PDF là một khung vẽ 2D độc lập với độ phân giải được xây dựng trên cùng một mô hình hình ảnh như PostScript. Nó có thể vẽ các đường thẳng, đường cong, các vùng được tô màu, gradient và các mẫu lặp lại, tất cả dưới dạng các vector sắc nét ở mọi mức thu phóng và in ở độ phân giải đầy đủ của thiết bị. Nếu bạn đang vẽ một logo, một biểu đồ, một hình mờ, hoặc một đường viền chứng chỉ, đường dẫn vector hầu như luôn là khối cơ bản (primitive) phù hợp, và nó nhỏ gọn và sắc nét hơn so với hình ảnh raster hóa mà nhiều chương trình thường sử dụng.

Bài viết này đi sâu vào mô hình vector như cách ISO 32000-1 định nghĩa nó và trình bày các lệnh gọi PDFlibPas tương ứng. Mục tiêu là làm cho thông số kỹ thuật trở nên cụ thể, bởi vì API ánh xạ chặt chẽ với nó, và việc hiểu một bên sẽ giúp bạn biết bên còn lại.

Trang là một cỗ máy tạo đường dẫn

ISO 32000-1 §8.5 mô tả đồ họa trong hai giai đoạn không bao giờ chồng chéo lên nhau. Đầu tiên bạn xây dựng một đường dẫn (path), vốn là hình học thuần túy không có kết quả hiển thị trực quan. Sau đó bạn vẽ (paint) đường dẫn đó trong một thao tác duy nhất để kẻ viền (stroke) phác thảo của nó, tô (fill) phần bên trong của nó, hoặc thực hiện cả hai. Không có gì xuất hiện trên trang trong quá trình xây dựng. Đường dẫn là một chuỗi điểm và đoạn thẳng trừu tượng được giữ trong trạng thái đồ họa (graphics state) cho đến khi một toán tử vẽ tiêu thụ nó, tại thời điểm đó nó được kết xuất và bị hủy bỏ.

Một đường dẫn được tạo thành từ một hoặc nhiều đường dẫn con (subpath). Một đường dẫn con bắt đầu tại một điểm và phát triển bằng cách nối thêm các đoạn: các đường thẳng, các đường cong Bezier bậc ba, và trên một số nền tảng là toàn bộ các hình chữ nhật được thêm vào dưới dạng đường dẫn con khép kín của riêng chúng. Trong PDFlibPas, bạn mở một đường dẫn bằng StartPath để thiết lập điểm bắt đầu, sau đó mở rộng nó bằng AddLineToPath and AddCurveToPath. Mỗi cuộc gọi tiến một điểm hiện tại ẩn, vì vậy đoạn tiếp theo tiếp tục từ nơi đoạn cuối cùng kết thúc. ClosePath vẽ một đoạn thẳng cuối cùng quay lại điểm bắt đầu của đường dẫn con, điều này rất quan trọng đối với việc kẻ viền vì nó tạo ra một điểm nối đường thực sự tại đỉnh khép kín thay vì hai đầu mút lỏng lẻo.

// 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

Các đường cong sử dụng AddCurveToPath, lệnh này nhận hai điểm kiểm soát Bezier và một điểm kết thúc: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Đường cong chạy từ điểm hiện tại đến (EndX, EndY), được kéo về phía hai điểm kiểm soát dọc theo đường đi. Các cung tròn có sẵn thông qua AddArcToPath(CenterX, CenterY, TotalAngle), trong đó bán kính được lấy từ khoảng cách giữa điểm hiện tại và tâm, và công cụ xuất ra cung dưới dạng một chuỗi các đoạn Bezier. Các hình chữ nhật có một lối tắt, AddBoxToPath(Left, Top, Width, Height), giúp nối thêm một hình chữ nhật khép kín hoàn chỉnh làm đường dẫn con của riêng nó mà không cần StartPath phía trước.

Hai quy tắc tô, và tại sao chúng không thống nhất

Khi bạn tô một đường dẫn tự cắt chính nó hoặc chứa một vòng lặp bên trong, bộ kết xuất cần một quy tắc để quyết định vùng nào nằm bên trong hình dạng và vùng nào là lỗ hổng. Tiêu chuẩn ISO 32000-1 §8.5.3.3 định nghĩa hai quy tắc, và chúng có thể vẽ cùng một hình học theo cách khác nhau. Quy tắc số vòng dây khác không (nonzero winding rule) đếm các giao lộ có dấu của một tia phát ra từ một điểm kiểm tra đến vô cực, cộng một cho mỗi đoạn giao cắt từ trái sang phải và trừ một cho mỗi đoạn giao cắt theo hướng ngược lại; điểm nằm bên trong khi tổng số khác không. Quy tắc chẵn-lẻ (even-odd rule) bỏ qua hướng và chỉ đơn giản đếm số lần giao cắt, gọi điểm nằm bên trong khi số lượng đếm là số lẻ.

Trường hợp cổ điển nơi chúng phân kỳ là một hình dạng có một lỗ, như hình bánh donut hay một vòng đệm. Vẽ một ranh giới bên ngoài và một ranh giới bên trong nằm trong nó. Theo quy tắc chẵn-lẻ, vòng lặp bên trong luôn tạo ra một lỗ hổng, bởi vì bất kỳ điểm nào giữa hai ranh giới đều bị cắt qua một lần và bất kỳ điểm nào bên trong vòng lặp bên trong bị cắt qua hai lần. Theo quy tắc số vòng dây khác không, lỗ hổng chỉ xuất hiện nếu vòng lặp bên trong quay theo hướng ngược lại với vòng lặp bên ngoài; quay chúng theo cùng một hướng và các vòng dây sẽ củng cố lẫn nhau thay vì triệt tiêu, và vùng bên trong sẽ được tô kín. Một ngôi sao năm cánh được vẽ dưới dạng một đường phác thảo tự giao nhau cho thấy cùng một sự phân chia: chẵn-lẻ để trống ngũ giác trung tâm trong khi số vòng dây khác không tô kín nó.

PDFlibPas chọn quy tắc bằng lệnh gọi bạn thực hiện để vẽ, chứ không phải bằng một cờ. DrawPath tô theo quy tắc số vòng dây khác không; DrawPathEvenOdd tô theo quy tắc chẵn-lẻ. Cả hai đều có cùng chế độ số nguyên: 0 chỉ kẻ viền phác thảo, 1 chỉ tô, và 2 tô và kẻ viền. Quy tắc chẵn-lẻ là công cụ dễ dàng hơn để đục lỗ chính xác vì nó không yêu cầu bạn phải quản lý hướng đường dẫn con.

// 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

Gradient trục thay đổi màu sắc dọc theo một đường

Một màu tô phẳng là một giá trị duy nhất trên toàn bộ vùng. Gradient thay đổi màu sắc liên tục, và loại đơn giản nhất là gradient trục (axial) hoặc tuyến tính (linear). Tiêu chuẩn ISO 32000-1 §8.7.4.5 chỉ định nó là dạng shading trục Loại 2 (Type 2 axial shading): bạn đưa ra hai điểm xác định một trục, một màu bắt đầu tại điểm thứ nhất và một màu kết thúc tại điểm thứ hai, và bộ kết xuất sẽ nội suy màu dọc theo trục đó. Mọi điểm trong vùng được tô sẽ nhận màu từ phép chiếu vuông góc của nó lên trục, vì vậy gradient chạy thành các dải vuông góc với đường thẳng nối giữa hai điểm.

Trong PDFlibPas, một gradient là một tài nguyên tài liệu được đặt tên mà bạn tạo một lần và sau đó chọn làm màu vẽ hoạt động. NewRGBAxialShader đăng ký nó. Chữ ký là NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): hai điểm đầu trục, các bộ ba RGB tại mỗi đầu dưới dạng giá trị trong khoảng từ 0 đến 1, và một cờ Extend. Với Extend được đặt thành 1, các màu kết thúc sẽ tiếp tục dưới dạng tô màu đặc vượt ra ngoài các điểm đầu trục, đây là điều bạn thường muốn để các góc của một vùng nằm ngoài trục không bị bỏ trống; 0 giữ nguyên chúng không đổi. Sau khi shader tồn tại, bạn liên kết nó bằng SetFillShader cho các vùng được tô, SetLineShader cho các nét phác thảo, hoặc SetTextShader cho văn bản. Liên kết này vẫn hoạt động cho các lệnh gọi vẽ tiếp theo, vì vậy đường dẫn bạn vẽ tiếp theo sẽ nhận gradient thay vì một màu phẳng.

// 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

Mẫu lát gạch (tiling pattern) lặp lại một ô

Trong khi gradient thay đổi một màu duy nhất một cách mượt mà, một mẫu lát gạch (tiling pattern) lặp lại một tác phẩm nghệ thuật nhỏ trên một vùng. ISO 32000-1 §8.7.3.1 định nghĩa một mẫu lát gạch là một ô mẫu (pattern cell), một phần nội dung độc lập, mà bộ kết xuất nhân bản trên một lưới cố định để lát kín khu vực đang được vẽ. Đây là cách bạn xây dựng các nét gạch mặt cắt cho bản vẽ kỹ thuật, một mô-típ thương hiệu lặp lại đằng sau một tiêu đề, hoặc một nền có hoa văn kết cấu vẫn sắc nét theo vector và chiếm dung lượng cực nhỏ bất kể khu vực lớn thế nào, bởi vì ô được lưu trữ một lần và được tham chiếu ở mọi nơi.

PDFlibPas xây dựng ô mẫu từ nội dung trang được chụp lại. Bạn chụp một trang hoặc một vùng bằng CapturePage, chuyển bản chụp thành một mẫu được đặt tên bằng NewTilingPatternFromCapturedPage(PatternName, CaptureID), và sau đó chọn mẫu đó làm màu tô hiện tại bằng SetFillTilingPattern(PatternName). Từ thời điểm đó trở đi, bất kỳ đường dẫn nào bạn tô đều được vẽ bằng ô lặp lại thay vì một màu phẳng, hoạt động chính xác như một cách tô shader nhưng với một ô lát gạch làm nguồn vẽ. Trình tự này phức tạp hơn một lệnh gọi đơn lẻ, vì vậy nếu bước chụp còn xa lạ, hãy coi mẫu này như một thao tác hai giai đoạn: tạo ô được chụp trước, sau đó liên kết nó làm màu tô theo tên trước khi vẽ vùng bạn muốn lát gạch.

Kết hợp các khối cơ bản lại với nhau

Các mảnh ghép kết hợp trực tiếp với nhau. Một hình Bezier được tô là một đường dẫn các đường cong được vẽ bằng DrawPath. Cùng một đường phác thảo được vẽ bằng DrawPathEvenOdd sau khi thêm một vòng lặp bên trong cho thấy một lỗ hổng mà kiểu tô vòng dây (winding fill) sẽ che kín. Một hình chữ nhật được tô gradient là một hộp được liên kết với một shader. Ví dụ bên dưới vẽ cả ba theo thứ tự để sự khác biệt giữa hai quy tắc tô hiển thị trên cùng một trang, sau đó đặt một bảng gradient bên dưới chúng.

// 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);

Hai chi tiết đáng được lưu giữ. Lệnh gọi vẽ quyết định quy tắc tô, vì vậy sự lựa chọn giữa DrawPathDrawPathEvenOdd là sự lựa chọn giữa số vòng dây khác không và chẵn-lẻ, và đối với các hình dạng có lỗ, quy tắc chẵn-lẻ giúp bạn không phải lập luận về hướng của đường dẫn con. Trạng thái đồ họa được lấy mẫu tại thời điểm bạn vẽ: thiết lập màu sắc, độ rộng đường nét và liên kết shader của bạn trước lệnh gọi vẽ, vì đó là trạng thái mà công cụ đọc. Xây dựng trước, định cấu hình trạng thái, vẽ cuối cùng, và mô hình vector hoạt động dự đoán được mỗi lần.

Từ đây, các bước tiếp theo tự nhiên là đọc các vector và văn bản ngược lại từ một tài liệu hiện có, được đề cập trong our article on text, image, and font extraction, và kết xuất cùng một mô hình vẽ sang một context thiết bị Windows để xem trước trên màn hình và in ấn, được đề cập trong the print and preview walkthrough. Các lệnh gọi đường dẫn, shader và pattern được mô tả ở đây được cung cấp như một phần của Delphi PDF Library cùng với các API văn bản, hình ảnh, biểu mẫu và chữ ký được đề cập ở những nơi khác trên blog này.