คุณใส่โลโก้ขนาด 600×400 พิกเซลลงในส่วนหัวของใบแจ้งหนี้ที่สร้างขึ้น มันดูพอดีบนจอพัฒนา 96 DPI ของคุณ แต่หนึ่งสัปดาห์ต่อมา ลูกค้าบนโน้ตบุ๊กความละเอียดสูงกลับบอกว่ามันพิมพ์ออกมาขนาดเท่าแสตมป์ พิกเซลไม่ได้เปลี่ยน สิ่งที่เปลี่ยนคือสมมติฐานที่ว่าจำนวนพิกเซลเท่ากับขนาดทางกายภาพ และใน OOXML มันไม่ใช่ รูปภาพในสเปรดชีตเก็บมิติไว้ใน EMU และจนกว่าคุณจะคิดด้วย EMU หรือหน่วยจริงที่แมปเข้ากันได้ งานจัดวางของคุณก็ยังขึ้นอยู่กับ DPI ที่เครื่องเรนเดอร์สมมติเอา
HotXLS เป็นคอมโพเนนต์สเปรดชีต VCL แบบดั้งเดิมสำหรับ Delphi และ C++Builder ที่อ่านและเขียน XLS และ XLSX ได้โดยไม่ต้องมี Excel หรือการพึ่งพา COM ตั้งแต่ v2.91.0 วัตถุรูปภาพของ XLSX ไม่บังคับให้คุณคำนวณหน่วยเองอีกต่อไป เพราะนอกจาก EMU ดิบแล้ว ยังเปิดเผยความกว้างและความสูงเป็นเซนติเมตร นิ้ว และพอยต์ พร้อมเมธอด Scale ที่ปรับขนาดตามเปอร์เซ็นต์และเลือกล็อกอัตราส่วนภาพได้ บทความนี้อธิบายว่า EMU คืออะไรจริง ๆ ทำไม DrawingML ถึงเลือกใช้ และจะใช้ชุดคุณสมบัติเรขาคณิตใหม่เพื่อวางรูปตามขนาดจริงแทนการนับพิกเซลที่ไว้ใจไม่ได้อย่างไร
EMU คืออะไร และทำไม DrawingML ถึงใช้มัน
EMU ย่อมาจาก English Metric Unit และเป็นหน่วยความยาวฐานของ DrawingML ซึ่งเป็นเลเยอร์การวาดที่ใช้ร่วมกันทั้งตระกูล Office Open XML (ECMA-376, Part 1, §20) EMU ถูกกำหนดให้มีสัดส่วนเป็น 914400 EMU ต่อหนึ่งนิ้ว และ 360000 EMU ต่อหนึ่งเซนติเมตร ค่าคงที่สองตัวนี้คือเหตุผลทั้งหมดที่หน่วยนี้มีอยู่ 914400 หารได้ด้วย 2, 3, 4, 5, 6, 8, 9, 10, 12 และอีกมากมาย โดยแยกตัวประกอบได้เป็น 26 × 32 × 52 × 127 เพราะ 1 นิ้ว = 2.54 เซนติเมตรแบบเป๊ะ ๆ การเลือกหน่วยที่หารได้ทั้ง 360000 และเป็นเศษส่วนที่สะอาดของ 914400 ทำให้ฟอร์แมตแสดงนิ้ว เซนติเมตร และพอยต์เป็น จำนวนเต็ม ได้โดยไม่ต้องปัดเศษที่ขอบหน่วย ถ้าใช้ชนิดทศนิยมแบบ "1.27 cm" ก็อาจเกิดความคลาดเคลื่อนได้ แต่ EMU เก็บ 457200 ไว้และยังคงแม่นยำ
หน่วยอีกตัวที่สำคัญคือพอยต์ 1 พอยต์เท่ากับ 1/72 นิ้ว ดังนั้นจึงมี 12700 EMU ต่อพอยต์ (914400 / 72) Excel เองคิดเรื่องความสูงของแถว ขนาดฟอนต์ และระยะขอบด้วยหน่วยนี้อยู่เบื้องหลัง ซึ่งเป็นเหตุผลว่าทำไมการเปิดเผยเรขาคณิตของรูปภาพเป็นพอยต์จึงมีประโยชน์ เมื่อคุณต้องการให้รูปวางตรงกับเมตริกของข้อความแทนที่จะอิงไม้บรรทัดบนกระดาษ HotXLS เข้ารหัสความสัมพันธ์ทั้งสี่แบบเป็นค่าคงที่หน่วยในไลบรารี:
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)
บรรทัดสุดท้ายนี่แหละคือแก่นของปัญหาแสตมป์ พิกเซลจะมีขนาดทางกายภาพก็ต่อเมื่อคุณกำหนด DPI และ 9525 EMU คือขนาดของพิกเซล เมื่ออยู่ที่ 96 DPI โดยเฉพาะ Excel ใช้ DPI การเรนเดอร์เริ่มต้นที่ 96 ดังนั้นรูปขนาด 100 พิกเซลจะลงเอกสารที่ 100 × 9525 = 952500 EMU ≈ 2.54 cm ในสภาพแวดล้อมเริ่มต้น แต่ไม่มีอะไรในไฟล์รับประกันว่าผู้เปิดไฟล์จะใช้ 96 การเขียนด้วยหน่วยจริงจึงตัดความกำกวมนี้ออกไป 4 cm ก็คือ 4 cm ไม่ว่าหน้าจอจะเป็น 96 หรือ 220 DPI
ส่วนเรขาคณิตของ `TXLSXImage`
รูปภาพที่ฝังใน HotXLS คือ TXLSXImage การเก็บข้อมูลหลักของมันคือฟิลด์จำนวนเต็มสองตัว WidthEMU และ HeightEMU ที่ยึดกับ Row และ Col แบบเริ่มนับจาก 1 ซึ่งเป็นเซลล์มุมซ้ายบนที่รูปยึดอยู่ คุณสมบัติหน่วยจริงเป็นมุมมองที่คำนวณจากฟิลด์ EMU เหล่านั้น ไม่ใช่สถานะคนละชุด การอ่าน WidthCM จะหาร EMU ด้วย 360000 และการเขียนจะคูณแล้วปัดกลับ ดังนั้นทุกมิติที่คุณตั้งจึงเป็นเพียงอีกชื่อหนึ่งของค่า EMU เดิม:
WidthInch/HeightInch: EMU ÷ 914400WidthCM/HeightCM: EMU ÷ 360000WidthPt/HeightPt: EMU ÷ 12700WidthEMU/HeightEMU: แหล่งที่มาของความจริงในรูปจำนวนเต็ม
คุณเพิ่มรูปด้วย AddImage(ARow, ACol, AData, AFormat) โดยส่งไบต์ที่เข้ารหัสดิบและ TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif, หรือ xlsxImageBmp) เมธอดจะคืนดัชนีแบบเริ่มนับจาก 0 ในคอลเลกชัน Images ของเวิร์กชีต นอกจากนี้ยังมี AddImageFromFile(ARow, ACol, AFileName) ที่เดารูปแบบจากนามสกุลไฟล์ด้วย สังเกตฐานดัชนีไว้ด้วย AddImage คืนค่าเริ่มจาก 0 และ Images[] ก็เริ่มจาก 0 ซึ่งตั้งใจให้ต่างจากกริด Cells[Row, Col] ที่เริ่มจาก 1 ดังนั้นอย่าสมมติว่าทั้งสองอย่างตรงกัน
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;
รูปภาพที่สร้างใหม่จะมีค่าเริ่มต้น 100×100 พิกเซล หรือ 952500 EMU เป็นสี่เหลี่ยมจัตุรัส ซึ่งประมาณเป็นกรอบ 2.54 cm ที่ 96 DPI ค่าตั้งต้นนี้มีไว้เพื่อให้เห็นรูปแม้คุณลืมกำหนดขนาด แต่สำหรับเลย์เอาต์จริงทุกกรณีควรตั้งขนาดทางกายภาพชัดเจนแทนที่จะพึ่งค่าที่แปลงมาจากพิกเซล
การปรับสเกลและตัวเลือกล็อกอัตราส่วนภาพ
เมื่อคุณต้องการปรับขนาดตามมิติเดิมแทนที่จะตั้งเป้าหมายแบบสัมบูรณ์ เช่น ย่อรูปแผนภูมิให้เหลือ 60% จากขนาดที่นำเข้า ให้ใช้ Scale:
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
APercent คือเปอร์เซ็นต์ที่ 100 หมายถึงไม่เปลี่ยน 150 คือขยายขึ้นครึ่งหนึ่ง 50 คือย่อครึ่งหนึ่ง เมื่อ AKeepAspect อยู่ที่ค่าเริ่มต้น True ทั้งความกว้างและความสูงจะคูณด้วยตัวประกอบเดียวกัน ทำให้อัตราส่วนคงเดิม และรูป 4×3 เซนติเมตรจะกลายเป็น 6×4.5 เซนติเมตรหลัง Scale(150) หากส่ง False จะปรับเฉพาะความกว้าง ความสูงจะคงเดิมทุกประการ ความไม่สมมาตรนี้ตั้งใจไว้ เพราะถ้าคุณต้องการยืดเพียงแกนเดียว เครื่องมือที่ถูกต้องคือ setter WidthCM/HeightCM แบบชัดเจน ส่วนแขนงที่ไม่รักษาอัตราส่วนของ Scale มีไว้สำหรับกรณีแคบ ๆ ที่ต้องการปรับเฉพาะความกว้าง อ่าน Scale(150, False) ว่าเป็นการยืดทั้งสองมิติอิสระได้ง่ายและอาจทำให้แปลกใจ ดังนั้นถ้าคุณต้องการสองมิติที่เป็นอิสระจริง ๆ ให้ใช้ setter
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
พฤติกรรมเล็ก ๆ ที่ควรรู้คือ Scale(100) จะตัดทางออกและกลับทันทีโดยไม่แตะฟิลด์ใด ๆ ดังนั้นเรียกมันในลูปได้อย่างปลอดภัยแม้เปอร์เซ็นต์อาจเป็น 100 และเพราะเรขาคณิตถูกเก็บเป็น EMU แบบจำนวนเต็ม ทุก setter จึงมีการปัดเศษ เมื่อส่งค่ากลับไปกลับมาผ่านเซนติเมตรแบบเศษส่วนจึงอาจคลาดไปเล็กน้อยในระดับเศษส่วนของ EMU ซึ่งเล็กกว่าที่จะมองเห็น แต่ก็ควรรู้ไว้หากคุณจะตรวจสอบความเท่ากันแบบเป๊ะ ๆ ในเทสต์ สำหรับการควบคุมแบบแม่นระดับพิกเซลจริง ๆ ให้ตั้ง WidthEMU และ HeightEMU โดยตรงและข้ามการแปลงหน่วยไปเลย
การอ่านเรขาคณิตกลับออกมา
คอลเลกชันรูปภาพสามารถสอบถามได้ ซึ่งสำคัญเมื่อคุณโหลดเวิร์กบุ๊กที่มีอยู่แล้วและต้องตรวจสอบหรือปรับสิ่งที่มีอยู่ แทนที่จะดูเฉพาะสิ่งที่เพิ่งเพิ่ม Images.Count นับรูปทุกภาพบนชีต Images[i] เข้าถึงแบบเริ่มนับจาก 0 และ FindAt(ARow, ACol) คืนรูปที่ยึดกับเซลล์ที่ระบุ หรือ nil ถ้าไม่มี นอกจากนี้ยังมี IndexOfCell สำหรับคืนดัชนีแทนวัตถุ และ DeleteAt / DeleteInRange สำหรับลบ
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;
เพราะคุณสมบัติหน่วยจริงเป็นมุมมองสด รูปที่นำเข้ามาจากเครื่องมืออื่นในขนาด EMU ใด ๆ จะรายงานมิติเป็นเซนติเมตรได้ทันทีโดยไม่ต้องแปลงเอง สิ่งนี้เข้ากันได้ดีกับโมเดลการวาดที่กว้างกว่า หากคุณวางทั้งแผนภูมิและรูปร่างควบคู่กับรูปแรสเตอร์ คู่มือประกอบเรื่อง คู่มือเกี่ยวกับแผนภูมิ รูปภาพ และ Excel drawings ใน Delphi กับ HotXLS จะอธิบายโมเดลจุดยึดที่วัตถุเหล่านี้ใช้ร่วมกัน
ระยะขอบการตั้งค่าหน้ากระดาษแบบเมตริก
ความตึงเครียดระหว่าง EMU กับหน่วยจริงแบบเดียวกันยังโผล่ขึ้นอีกชั้นที่ระดับหน้า OOXML และ Excel เก็บระยะขอบพิมพ์เป็น นิ้ว ซึ่งไม่สะดวกเลยถ้าเทมเพลตรายงานของคุณระบุเป็นมิลลิเมตรเหมือนงานส่วนใหญ่ในโลกนอกสหรัฐฯ v2.91.0 เพิ่มตัวห่อหุ้มแบบเซนติเมตรบนระยะขอบที่เป็นนิ้ว ได้แก่ MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, และ MarginFooterCM แต่ละตัวเป็นเพียงความสะดวกบาง ๆ รอบคุณสมบัติหน่วยนิ้วที่เกี่ยวข้อง โดยแปลงด้วยอัตราส่วนที่แม่นยำ 1 นิ้ว = 2.54 เซนติเมตร
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;
คุณสมบัติหน่วยนิ้ว (MarginLeft และตัวอื่น ๆ) ยังเป็นที่เก็บหลัก ดังนั้นคุณจะผสมสองแบบกันก็ได้ ตั้งค่าระยะขอบบนเป็นเซนติเมตรแล้วอ่านกลับเป็นนิ้ว หรือสลับกัน ไฟล์ที่เขียนลงดิสก์จะเหมือนเดิมทุกประการ การแปลงเป็นเพียงการคูณ 2.54 ตรง ๆ ไม่ได้ปัดไปกริดหยาบ ๆ ดังนั้น 2 เซนติเมตรก็ยังเป็น 2 เซนติเมตรเต็มความละเอียดของ double นี่คือปรัชญาความสะดวกแบบเมตริกเดียวกับเรขาคณิตของรูปภาพ ฟอร์แมตพูดภาษานิ้วอยู่ข้างใน แต่ไลบรารีเปิดให้คุณเขียนด้วยหน่วยที่สเปกของคุณใช้ หากต้องการจัดวางรายงานรอบ ๆ หัวเรื่อง บล็อกเมตาดาตา และยอดรวม ดู เซลล์ที่รวมและการจัดวางเทมเพลตรายงานใน HotXLS ซึ่งใช้ระยะขอบเหล่านี้ร่วมกับช่วงที่รวมและพื้นที่พิมพ์
หมายเหตุเกี่ยวกับสิ่งที่คุณสมบัติเรขาคณิตรับรองและไม่รับรอง
คุณสมบัติเรขาคณิตควบคุมขนาดที่ ประกาศ ของรูปในไฟล์ ซึ่งเป็นขนาดที่โปรแกรมที่รองรับมาตรฐานจะเรนเดอร์ออกมา พวกมันไม่ปรับตัวอย่างใหม่ให้กับไบต์ของรูปภาพ 50×50 พิกเซลที่กำหนดขนาดเป็น 8 เซนติเมตรจะถูกขยายและดูเป็นบล็อกเหมือนใน Excel ทุกประการ การกำหนดขนาดเป็นงานเลย์เอาต์ ไม่ใช่งานประมวลผลภาพ ดังนั้นควรป้อนรูปต้นฉบับที่มีความละเอียดพอสำหรับขนาดทางกายภาพที่คุณต้องการ ไลบรารีนี้ก็ไม่เข้ารหัสใหม่ให้กับฟอร์แมตเช่นกัน ไบต์ที่คุณส่งให้ AddImage จะถูกเก็บและเขียนผ่านตามเดิมพร้อม TXLSXImageFormat ที่คุณประกาศ ถ้าส่งไบต์ JPEG แต่ติดป้ายเป็น xlsxImagePng คุณจะได้ไฟล์ที่ Excel เปิดไม่ได้ ดังนั้นเมื่อทำได้ให้ AddImageFromFile เดารูปแบบจากนามสกุลไฟล์
เมื่อเข้าใจแนวคิดเดียวที่อยู่ข้างใต้ทั้งหมดแล้ว เรื่องนี้ก็ไม่ใช่อะไรแปลก ใน OOXML ขนาดทางกายภาพคือค่าจริง ส่วนพิกเซลเป็นเงาที่ขึ้นกับ DPI เท่านั้น เขียนรูปภาพและระยะขอบด้วยเซนติเมตร นิ้ว หรือพอยต์ ให้ HotXLS แปลงเป็น EMU ที่แม่นยำ แล้วใบแจ้งหนี้และรายงานของคุณจะพิมพ์ออกมาในขนาดเดียวกันบนทุกเครื่องที่เปิดไฟล์
API สำหรับเรขาคณิตของรูปภาพ การปรับสเกล และระยะขอบแบบเมตริกที่อธิบายในที่นี้มาพร้อมกับ คอมโพเนนต์สเปรดชีต HotXLS สำหรับ Delphi ซึ่งอ่านและเขียน XLS และ XLSX จาก Delphi และ C++Builder โดยไม่ต้องติดตั้ง Excel