Bài viết kỹ thuật

Hình học ảnh HotXLS trong Delphi: EMU, cm và thu phóng

Bạn thả một logo 600×400 pixel vào phần đầu của hóa đơn được tạo ra, nó trông ổn trên màn hình phát triển 96-DPI của bạn, rồi một tuần sau một khách hàng dùng laptop độ phân giải cao báo rằng bản in chỉ to bằng con tem thư. Pixel không hề đổi. Thứ đổi là giả định rằng số pixel tương đương với kích thước vật lý, trong OOXML thì không phải vậy. Ảnh trong bảng tính mang kích thước của nó bằng EMU, và cho đến khi bạn tư duy bằng EMU - hoặc bằng các đơn vị thực tế quy đổi gọn sang nó - bố cục của bạn vẫn bị chi phối bởi DPI mà máy dựng hình tình cờ chọn

HotXLS là một thành phần bảng tính VCL gốc cho Delphi và C++Builder, đọc và ghi XLS và XLSX mà không cần Excel hay bất kỳ phụ thuộc COM nào. Từ v2.91.0, đối tượng ảnh XLSX không còn bắt bạn tự tính đơn vị bằng tay nữa: bên cạnh EMU thô, nó hiển thị chiều rộng và chiều cao theo centimet, inch và point, cùng phương thức Scale thay đổi kích thước theo phần trăm với tùy chọn khóa tỷ lệ khung hình. Bài viết này giải thích EMU thực chất là gì, vì sao DrawingML chọn nó, và cách dùng lớp hình học mới để đặt ảnh theo kích thước vật lý thay vì theo số pixel không đáng tin

EMU là gì và vì sao DrawingML dùng nó

EMU là viết tắt của English Metric Unit, và nó là đơn vị chiều dài cơ bản của DrawingML, lớp vẽ được chia sẻ trên toàn bộ họ Office Open XML (ECMA-376, Part 1, §20). Một EMU được định nghĩa sao cho có đúng 914400 EMU trên mỗi inch360000 EMU trên mỗi centimet. Hai hằng số đó là toàn bộ lý do đơn vị này tồn tại. 914400 chia hết cho 2, 3, 4, 5, 6, 8, 9, 10, 12 và nhiều số khác; nó phân tích thành 26 × 32 × 52 × 127. Vì 1 inch = 2,54 cm chính xác, chọn một đơn vị chia hết cho cả 360000 và một phần sạch của 914400 cho phép định dạng biểu diễn inch, centimet và point dưới dạng số nguyên mà không phải làm tròn ở ranh giới đơn vị. Nơi mà giá trị dấu phẩy động "1,27 cm" sẽ trôi đi, EMU lưu 457200 và vẫn chính xác

Đơn vị khác quan trọng ở đây là point. Một point kiểu typography bằng 1/72 inch, nên có 12700 EMU cho mỗi point (914400 / 72). Point là cách Excel tự nghĩ về chiều cao hàng, cỡ chữ và lề ở bên trong, vì vậy việc hiển thị hình học ảnh theo point hữu ích khi bạn muốn ảnh thẳng hàng với số đo văn bản hơn là với thước in. HotXLS mã hóa cả bốn quan hệ này thành hằng số đơn vị trong thư viện:

const
  XlsxEmuPerInch  = 914400;  // 1 inch
  XlsxEmuPerCm    = 360000;  // 1 centimetre
  XlsxEmuPerPoint = 12700;   // 1 point (1/72 inch)
  XlsxEmuPerPixel = 9525;    // 1 pixel at 96 DPI (914400 / 96)

Dòng cuối là gốc rễ của lỗi ảnh to bằng tem thư. Một pixel chỉ có kích thước vật lý khi bạn cố định DPI, và 9525 EMU là kích thước của một pixel cụ thể tại 96 DPI. DPI dựng hình mặc định của Excel là 96, nên ảnh 100 pixel sẽ rơi vào 100 × 9525 = 952500 EMU ≈ 2,54 cm trên cấu hình mặc định - nhưng không có gì trong tệp đảm bảo người xem dùng 96 DPI. Soạn thảo bằng đơn vị thực là hết mơ hồ: 4 cm vẫn là 4 cm dù màn hình là 96 hay 220 DPI

Lớp hình học TXLSXImage

Một ảnh nhúng trong HotXLS là TXLSXImage. Lưu trữ chuẩn của nó là hai trường số nguyên, WidthEMUHeightEMU, neo tại RowCol đánh số từ 1 (ô góc trên bên trái mà ảnh bám vào). Các thuộc tính đơn vị thực chỉ là khung nhìn được tính toán trên các trường EMU đó, không phải trạng thái riêng - đọc WidthCM chia EMU cho 360000, còn ghi nó thì nhân lên rồi làm tròn lại. Vì vậy mọi kích thước bạn đặt chỉ là một cách gọi khác của cùng một giá trị EMU nền tảng:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - nguồn sự thật dạng số nguyên

Bạn thêm ảnh bằng AddImage(ARow, ACol, AData, AFormat), truyền vào các byte mã hóa thô và một TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, hoặc xlsxImageBmp); hàm này trả về chỉ số zero-based trong collection Images của worksheet. Cũng có AddImageFromFile(ARow, ACol, AFileName), hàm suy ra định dạng từ phần mở rộng file. Lưu ý cơ sở chỉ số: AddImage trả về zero-based và Images[] cũng zero-based, đối lập có chủ ý với lưới Cells[Row, Col] đánh số từ 1, nên đừng cho rằng hai bên khớp nhau

var
  Sheet: TXLSXWorksheet;
  Img: TXLSXImage;
  Idx: Integer;
begin
  Sheet := Workbook.Sheets.Add('Images');

  // Anchor a PNG at row 3, column 2; AddImage returns a 0-based index.
  Idx := Sheet.AddImage(3, 2, LogoBytes, xlsxImagePng);

  Img := Sheet.Images[Idx];
  Img.WidthCM := 4.0;    // 4 cm wide  -> 1440000 EMU
  Img.HeightCM := 3.0;   // 3 cm tall  -> 1080000 EMU

  // Same geometry, read back in other units.
  // Img.WidthPt  is now 113.39 pt, Img.WidthInch is 1.5748 in.
end;

Một ảnh mới tạo mặc định là 100×100 pixel, tức 952500 EMU vuông, xấp xỉ một hộp 2,54 cm ở 96 DPI. Mặc định đó tồn tại để ảnh vẫn hiện ra nếu bạn quên đặt kích thước, nhưng với mọi bố cục thực tế bạn nên đặt kích thước vật lý rõ ràng thay vì dựa vào mặc định suy ra từ pixel

Tỷ lệ và cờ khóa khung hình

Khi bạn muốn thay đổi kích thước tương đối so với kích thước hiện tại thay vì theo một mục tiêu tuyệt đối - ví dụ thu nhỏ ảnh biểu đồ còn 60% kích thước khi nhập vào - hãy dùng Scale:

procedure Scale(APercent: Double; AKeepAspect: Boolean = True);

APercent là một phần trăm, trong đó 100 nghĩa là không đổi, 150 là phóng lớn thêm một nửa, 50 là giảm còn một nửa. Với AKeepAspect ở giá trị mặc định True, cả chiều rộng lẫn chiều cao đều nhân với cùng một hệ số, nên tỷ lệ được giữ nguyên và ảnh 4×3 cm trở thành 6×4,5 cm sau Scale(150). Truyền False thì chỉ chiều rộng được co giãn - chiều cao giữ nguyên đúng như cũ. Sự bất đối xứng này là chủ ý: khi bạn muốn kéo giãn riêng một trục, công cụ đúng là các setter WidthCM/HeightCM, còn nhánh không giữ tỷ lệ của Scale dành cho trường hợp hẹp hơn là chỉ chỉnh chiều rộng. Dễ đọc Scale(150, False) thành "kéo giãn cả hai tự do" rồi bất ngờ, nên hãy dùng các setter khi thật sự muốn hai kích thước độc lập

Img.WidthCM := 4.0;
Img.HeightCM := 3.0;

Img.Scale(150);          // aspect locked: now 6.0 x 4.5 cm
Img.Scale(100);          // no-op, returns immediately

Img.Scale(50, False);    // width only: 3.0 cm wide, height unchanged at 4.5 cm

Một chi tiết nhỏ cần biết: Scale(100) sẽ thoát sớm và không chạm vào bất kỳ trường nào, nên có thể gọi vô điều kiện trong vòng lặp khi phần trăm có thể là 100. Và vì hình học được lưu bằng EMU dạng số nguyên, mọi setter đều làm tròn. Do đó, việc đi qua lại giữa các giá trị centimet phân số có thể lệch đi một phần rất nhỏ của EMU - nhỏ đến mức không thể thấy, nhưng nên biết nếu bạn từng kiểm tra bằng phép so sánh chính xác trong test. Nếu cần kiểm soát từng pixel, hãy đặt trực tiếp WidthEMUHeightEMU rồi bỏ qua hoàn toàn bước chuyển đổi đơn vị

Đọc ngược hình học

Collection ảnh có thể truy vấn được, và điều này quan trọng khi bạn nạp một workbook có sẵn rồi cần xem xét hoặc chỉnh phần đã có thay vì phần vừa thêm. Images.Count liệt kê mọi ảnh trên sheet, Images[i] đánh chỉ số zero-based cho chúng, và FindAt(ARow, ACol) trả về ảnh được neo tại một ô cụ thể - hoặc nil nếu không có ảnh nào. Cũng có IndexOfCell để lấy chỉ số thay vì đối tượng, và DeleteAt / DeleteInRange để xóa

var
  i: Integer;
  Img: TXLSXImage;
begin
  for i := 0 to Sheet.Images.Count - 1 do
  begin
    Img := Sheet.Images[i];
    Writeln(Format('[%d] R%dC%d  %.2f x %.2f cm  (%d x %d EMU)',
      [i, Img.Row, Img.Col, Img.WidthCM, Img.HeightCM,
       Img.WidthEMU, Img.HeightEMU]));
  end;

  Img := Sheet.Images.FindAt(3, 2);   // nil-check before use
  if Img <> nil then
    Img.Scale(80);
end;

Vì các thuộc tính đơn vị thực là những khung nhìn động, một ảnh được nhập từ công cụ khác với kích thước EMU nào đó sẽ báo ngay hình học của nó theo centimet - không cần bạn tự chuyển đổi. Điều này khớp tự nhiên với mô hình drawing rộng hơn; nếu bạn đặt cả biểu đồ và shape cùng với ảnh raster, hướng dẫn đi kèm về biểu đồ, ảnh và bản vẽ Excel trong Delphi sẽ bao quát mô hình neo mà các đối tượng đó chia sẻ

Lề thiết lập trang theo hệ mét

Căng thẳng giữa EMU và các đơn vị thực cũng xuất hiện ở mức ngoài hơn, tại trang in. OOXML và Excel lưu lề in bằng inch, điều này khá bất tiện nếu mẫu báo cáo của bạn được xác định bằng milimet như phần lớn thế giới ngoài Mỹ. v2.91.0 bổ sung các bao bọc theo centimet cho lề inch: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCMMarginFooterCM. Mỗi cái là một tiện ích mỏng trên thuộc tính inch tương ứng, chuyển đổi đúng theo tỷ lệ 1 inch = 2,54 cm

Sheet.MarginLeftCM := 2.0;     // 2 cm  == 0.7874 inch
Sheet.MarginRightCM := 2.0;
Sheet.MarginTopCM := 2.5;
Sheet.MarginBottomCM := 2.5;
Sheet.MarginHeaderCM := 1.0;
Sheet.MarginFooterCM := 1.0;

Các thuộc tính inch (MarginLeft và các thuộc tính khác) vẫn là nơi lưu trữ chuẩn, nên bạn có thể dùng lẫn hai cách - đặt lề trên bằng centimet rồi đọc lại bằng inch, hoặc ngược lại - và tệp ghi ra đĩa vẫn giống hệt. Việc chuyển đổi chỉ là nhân với 2,54, không làm tròn về một lưới thô nào cả, nên 2 cm vẫn là 2 cm với độ chính xác double đầy đủ. Đây cũng là cùng triết lý tiện dụng theo hệ mét như hình học ảnh: định dạng nói tiếng imperial ở bên dưới, còn thư viện cho phép bạn soạn theo đơn vị mà bản đặc tả của bạn dùng. Để bố trí phần báo cáo xung quanh - tiêu đề, khối metadata, tổng cộng - hãy xem ô gộp và bố cục mẫu báo cáo trong HotXLS, nơi các lề này được dùng cùng vùng gộp và vùng in

Lưu ý về điều hình học đảm bảo và không đảm bảo

Các thuộc tính hình học kiểm soát kích thước được khai báo của ảnh trong tệp - tức là kích thước mà một trình đọc tuân thủ chuẩn sẽ hiển thị. Chúng không nội suy lại byte ảnh; một PNG 50×50 pixel được đặt kích thước 8 cm vẫn sẽ được phóng lên và trông rỗ, đúng như trong Excel. Đặt kích thước là thao tác bố cục, không phải xử lý ảnh, vì vậy hãy cung cấp cho ảnh độ phân giải nguồn đủ cho kích thước vật lý bạn định dùng. Thư viện cũng không mã hóa lại định dạng: các byte bạn truyền vào AddImage được lưu và ghi ra nguyên trạng, cùng với TXLSXImageFormat bạn khai báo. Nếu truyền byte JPEG nhưng gắn nhãn xlsxImagePng thì bạn sẽ tạo ra một tệp Excel không mở được, nên hãy để AddImageFromFile suy ra định dạng từ phần mở rộng khi có thể

Mọi thứ này không hề phức tạp khi bạn nắm ý chính bên dưới nó: trong OOXML, kích thước vật lý là giá trị thật, còn pixel chỉ là cái bóng phụ thuộc DPI của nó. Soạn ảnh và lề theo centimet, inch hoặc point, để HotXLS ánh xạ chúng sang EMU chính xác, và hóa đơn cũng như báo cáo của bạn sẽ in ra cùng một kích thước trên mọi máy mở chúng

Các API hình học ảnh, thu phóng và lề theo hệ mét được mô tả ở đây đi kèm với thành phần bảng tính Delphi HotXLS, thành phần đọc và ghi XLS và XLSX từ Delphi và C++Builder mà không cần cài Excel