Delphi và Lazarus biên dịch cùng một Object Pascal, và sự tương đồng bề mặt đó chính là điều khiến việc chuyển một viewer giữa hai môi trường trở nên dễ đánh lừa. Hai bộ công cụ khác nhau ở ba điểm quan trọng đối với công việc PDF: kiểu string gốc là UTF-16 trong Delphi và UTF-8 trong một ứng dụng LCL; VCL và LCL là hai framework giao diện khác nhau với control, hộp thoại và định dạng streaming form riêng; và một file nhị phân Delphi nhắm đến Windows trong khi một file nhị phân FPC có thể hướng tới Linux hoặc macOS. Không khác biệt nào trong số đó lộ ra khi biên dịch. Một viewer xây dựng trên PDFium Component, vốn phát hành cả bản VCL và LCL từ một cây mã nguồn duy nhất, sẽ biên dịch sạch sẽ dưới Lazarus sau vài lần đổi tên unit và vài khối {$IFDEF FPC}. Các lỗi thực sự xuất hiện sau đó, khi dữ liệu thật và một lần triển khai thật phơi bày những giả định mà bản dựng Delphi âm thầm đưa ra
Bốn giả định đó chiếm phần lớn thời gian bị mất: mã hóa văn bản tại ranh giới giao diện, cám dỗ duy trì hai bản sao của form, cách một file nhị phân engine gốc được phân giải lúc chạy, và thời điểm chuyển văn bản thành giọng nói hết nền tảng để dựa vào khi SAPI không còn nữa. Mỗi vấn đề đều rẻ để xử lý nếu bạn biết trước nó sẽ đến, và đắt để truy tìm nếu không
Cùng một Pascal, payload chuỗi khác nhau
Kiểu string gốc của Delphi đã là UTF-16 kể từ năm 2009. Lazarus và Free Pascal mặc định dùng UTF-8 trong các ứng dụng LCL. Các API hướng văn bản của component nói UTF-16 thông qua kiểu WString, mà bản dựng FPC đặt bí danh thành WideString, vì vậy mọi ranh giới nơi văn bản đi qua giữa giao diện LCL của bạn và engine PDF đều là một điểm chuyển đổi
Các phép chuyển đổi diễn ra tự động trong các phép gán đơn giản, và phần lớn mã nguồn không bao giờ cần nghĩ đến chúng. Hai thói quen giữ cho lỗi mã hóa không xuất hiện. Truyền văn bản trực tiếp mà không thao tác ở mức byte: mã cắt một cụm từ tìm kiếm theo offset byte hoạt động đúng trong Delphi, nơi một Char là một đơn vị UTF-16, nhưng làm hỏng UTF-8 đa byte trong LCL. Và kiểm thử với dữ liệu ngoài ASCII ngay từ lần chạy đầu tiên. Một tên file tiếng Đức, một cụm từ tìm kiếm bằng chữ Kirin, một tên tác giả có dấu trong metadata tài liệu: dữ liệu kiểm thử thuần ASCII che giấu mọi lỗi mã hóa, bởi ASCII là phạm vi duy nhất nơi UTF-8 và UTF-16 khớp nhau byte theo ký tự. Lỗi vẫn có thật suốt thời gian đó; ASCII chỉ giữ nó vô hình cho đến khi một khách hàng ở Munich mở một file bạn chưa từng thử
Một khối điều kiện, không phải mỗi IDE một nhánh riêng
Sau khoảng chục IFDEF đầu tiên, mã nguồn bắt đầu trông như hai dự án khoác chung một repository, và việc tách nhánh theo từng IDE trông có vẻ hấp dẫn. Đó là bước đi sai. Những khác biệt thực sự thu gọn lại thành một khối khai báo dùng chung, và một nhánh riêng sẽ nhân đôi chi phí của mỗi lần sửa lỗi kể từ đó trở đi. Giữ lớp điều kiện này nhỏ đến mức này:
{$IFDEF FPC}
uses
LCLType, Forms, Graphics, Controls;
type
WString = WideString; // API văn bản của component là UTF-16
TBytes = array of Byte;
{$ELSE}
uses
Winapi.Windows, Vcl.Forms, Vcl.Graphics, Vcl.Controls;
{$ENDIF}
Mọi thứ bên dưới khối đó biên dịch giống hệt nhau trên cả hai IDE. Xử lý tài liệu, điều hướng trang, các lệnh gọi rendering: TPdf và TPdfView phơi bày cùng một bề mặt trong các bản VCL và LCL, vì vậy phần lớn viewer không bao giờ thấy một điều kiện biên dịch nào. Giữ được điều đó là kỷ luật cấu trúc chứ không phải một mẹo khéo léo. Logic PDF dùng chung sống trong các unit không kéo theo hộp thoại hay panel đặc thù cho framework nào. Số ít trường hợp thực sự khác nhau, như hộp thoại in và bộ chọn file với quy ước riêng của từng nền tảng, ẩn sau một giao diện mỏng được triển khai một lần cho mỗi framework. Khối IFDEF trở thành nơi duy nhất được phép chứa sự phân kỳ nền tảng trong tương lai, thay vì để các chỉ thị biên dịch rò rỉ ra bốn mươi unit
Xây dựng form bằng mã, không phải bằng hai trình thiết kế
Streaming form là nơi các dự án hai IDE âm thầm mục nát. Một file .dfm và một file .lfm tuyên bố mô tả cùng một form sẽ trôi dạt xa nhau theo từng thuộc tính cho đến khi hai bản dựng hành xử khác nhau vì những lý do không ai có thể so sánh được, bởi vì hai file thậm chí không cùng định dạng. Xây dựng viewer lúc chạy né tránh toàn bộ vấn đề này. Chỉ có một chuỗi constructor duy nhất, được quản lý phiên bản như mã thông thường, và nó đọc giống nhau trên cả hai nền tảng:
procedure TViewerForm.FormCreate(Sender: TObject);
begin
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.FitMode := pfmFitWidth;
if ParamCount > 0 then
begin
Pdf.FileName := ParamStr(1);
Pdf.Active := True; // mở tài liệu; PageCount hợp lệ sau bước này
end;
end;
Thứ tự chính xác của các phép gán đó ít quan trọng hơn dòng lệnh thực sự làm việc chính. PdfView.Pdf := Pdf gắn control hiển thị với component tài liệu, và từ điểm đó điều hướng trang qua PageNumber và hành vi khớp trang qua FitMode phản hồi giống hệt nhau dưới VCL và LCL. Có một điểm kỳ lạ giữa hai framework đáng biết trước khi người dùng báo nó như một lỗi: gán Zoom thủ công sẽ đưa FitMode về lại pfmNone trên cả hai framework. Vì vậy nếu thanh công cụ của bạn coi "fit width" là một tùy chọn được ghi nhớ, bạn phải gán lại chế độ khớp trang sau mỗi lần zoom bằng lập trình, nếu không tùy chọn đó sẽ âm thầm ngừng được ghi nhớ ngay lần đầu tiên mã chạm vào mức zoom
File nhị phân mà IDE chưa từng cảnh báo bạn
Component bọc lấy engine PDFium, vốn được phát hành dưới dạng file nhị phân nền tảng gốc, và file nhị phân đó là nguồn gốc của gần như mọi báo cáo kiểu "chạy được trong IDE, lỗi khi chạy từ shortcut đã cài đặt". Ba quy tắc chiếm phần lớn các trường hợp đó. Bitness phải khớp chính xác. Một file thực thi 32-bit không thể nạp một thư viện pdfium 64-bit, và thông báo mà hệ điều hành trả về ("module not found" trên một số phiên bản Windows) chủ động gây hiểu lầm, bởi vì file đó đang nằm ngay đó, cạnh file thực thi. Phân giải đường dẫn thư viện tương đối so với file thực thi, không bao giờ so với thư mục làm việc; một lần khởi chạy từ IDE và một lần khởi chạy từ shell khác nhau chính xác ở điểm đó, đó là lý do lỗi ẩn mình trong quá trình phát triển. Và bắt lỗi nạp thất bại trước khi tài liệu đầu tiên mở ra, rồi báo cáo nó kèm đường dẫn và kiến trúc kỳ vọng được ghi rõ ràng. Một ticket hỗ trợ ghi "thiếu file nhị phân PDFium 64-bit tại <path>" đóng lại trong vài phút. Một ticket ghi "viewer bị crash khi khởi động" biến thành cả tuần qua lại
Nhân tiện, hãy đánh số phiên bản cho file nhị phân engine song song với file thực thi. PDFium phát triển nhanh, và một trình cài đặt cập nhật ứng dụng nhưng để lại thư viện cũ trên đĩa sẽ tạo ra những lần crash không ai trong văn phòng bạn có thể tái hiện, chỉ vì lý do đơn giản là mọi máy trong văn phòng bạn tình cờ đều có cặp file khớp nhau. Hãy coi thư viện là một phần của build artifact, với cùng trình cài đặt, cùng dấu phiên bản và cùng đường dẫn rollback như file thực thi nạp nó
Đăng ký component trong IDE Lazarus
Việc xây dựng lúc chạy không cần bất kỳ đăng ký design-time nào cả, đó là cách thiết lập gọn gàng nhất cho một viewer tự xây giao diện của mình bằng mã. Khi bạn thực sự muốn các component xuất hiện trên palette của Lazarus để làm việc ở chế độ design-time, hãy cài đặt package và để unit đăng ký chuyên dụng của nó, PDFiumLazReg trong Lib/FPC/PDFiumLaz.lpk, xử lý việc đó. Unit đó được đánh dấu là design-time có chủ đích: nó tham chiếu đến các giao diện property-editor của IDE mà không bao giờ được liên kết vào file thực thi bạn phát hành
Làm sai điều này và triệu chứng là một ứng dụng phụ thuộc một cách khó hiểu vào các package của IDE, điều này lộ ra như một lỗi triển khai trên máy khách hàng đầu tiên chưa từng cài Lazarus
Giọng nói và trình đọc màn hình ngoài Windows
Chuyển văn bản thành giọng nói là tính năng duy nhất nơi câu chuyện đa nền tảng bị gãy, và nó gãy ở hệ điều hành, không phải ở component. SAPI, backend TTS thông thường trên Windows, chỉ tồn tại trên Windows. Một bản dựng Lazarus vẫn nhắm đến Windows giữ nguyên đầu ra SAPI đầy đủ và cùng hành vi tương thích NVDA mà bản gốc Delphi có, vì vậy một lần chuyển từ Windows sang Windows không mất gì ở đây, và một người dùng NVDA không thể phân biệt được hai bản dựng
Một mục tiêu Linux hoặc macOS lại là chuyện khác. Không có SAPI để gọi, vì vậy đầu ra âm thanh phải được nối lại với một dịch vụ giọng nói gốc trong khi các API đọc phía trên nó vẫn giữ nguyên. Sự tách biệt đó chính là lý do để đặt giọng nói sau một giao diện ngay từ commit đầu tiên: phân tích thứ tự đọc và con trỏ theo dõi từ đều trung lập với nền tảng và mang sang nguyên vẹn, và chỉ lớp mỏng thực sự tạo ra âm thanh mới phải thay đổi theo từng nền tảng. Bài viết về trình đọc hỗ trợ tiếp cận đi sâu vào cơ chế đọc đó
Danh sách kiểm tra tương đương trước khi coi việc chuyển đổi là xong
Lượt kiểm tra sau đây đã phát hiện những hồi quy có thật, được liệt kê đại khái theo thứ tự các lỗi thường xuất hiện. Mở một tài liệu có đường dẫn chứa ký tự ngoài ASCII. Tìm kiếm một cụm từ có ký tự ngoài ASCII và xác nhận các kết quả được tô sáng đúng vị trí. Thử cuộn bằng bánh xe chuột, chọn bằng kéo thả, và điều hướng trang bằng bàn phím trên từng bộ widget bạn phát hành, bởi vì xử lý focus và hành vi bánh xe là những góc phụ thuộc nhiều nhất vào bộ widget trong LCL. Kiểm tra rendering ở mức thu phóng màn hình 100%, 150% và 200%. Cuối cùng, chạy bản dựng đã cài đặt, không phải bản dựng từ IDE, trên một máy chưa từng cài IDE, bởi vì đó là bài kiểm tra duy nhất thực sự kiểm chứng việc phân giải file nhị phân một cách trung thực. Mọi thứ khác có thể đạt trong khi riêng bài kiểm tra này âm thầm thất bại
Thông lượng rendering được mang sang nguyên vẹn giữa hai bản, vì vậy cách tiếp cận caching từ bài viết về cache rendering và hiệu năng zoom áp dụng cho viewer LCL đúng y như đã viết cho bản VCL
Không điều gì trong số này khiến bản LCL trở thành phiên bản kém hơn. Bề mặt cốt lõi giống hệt nhau ở cả hai phía: TPdf, TPdfView, rendering, form, trích xuất văn bản và các API hỗ trợ tiếp cận hành xử giống nhau bất kể IDE nào biên dịch chúng. Mỗi khác biệt đáng theo dõi đều gắn với nền tảng chứ không phải gắn với bản dựng. Giọng nói SAPI chỉ dành cho Windows, các hộp thoại tuân theo quy ước riêng của từng framework, và file nhị phân phải khớp với kiến trúc mà nó được nạp vào. Làm đúng ranh giới mã hóa, form lúc chạy và việc phân giải file nhị phân, và phần còn lại của việc chuyển đổi chỉ là công việc máy móc mà trình biên dịch đã xử lý sẵn cho bạn
Các bản VCL và LCL được mô tả ở đây được phát hành cùng nhau dưới tên PDFium Component, với mã nguồn và API công khai giống hệt nhau cho Delphi, C++Builder và Lazarus/FPC