PDF Library for Delphi เรนเดอร์ HTML ลงในหน้า PDF ด้วยเลย์เอาต์สองมิติจริง display: flex และ display: grid ถูกวัดขนาดและวางตำแหน่งจริง แทนที่จะลดรูปลงเป็นบล็อกซ้อนกัน และเชิงอรรถถูกจองพื้นที่ไว้ที่ท้ายกล่องที่พกการอ้างอิงของมัน โดยการนับเลขยังคงต่อเนื่องกันข้ามคอลัมน์และหน้า จุดเข้าถึงคือจุดที่คุ้นเคยกันดี คือ DrawHTMLTextBox สำหรับกล่องเดียว และ DrawHTMLStory สำหรับการไหลแบบหลายคอลัมน์
เรื่องนี้สำคัญเพราะ HTML คือวิธีที่เนื้อหารายงานส่วนใหญ่มาถึงในปัจจุบัน เทมเพลตถูกเขียนขึ้นโดยคนที่เขียน CSS แดชบอร์ดถูกออกแบบเป็นการ์ด และตัวเรนเดอร์ที่ยุบแถว flex ลงเป็นบล็อกซ้อนกันสี่บล็อกอย่างเงียบๆ จะสร้างเอกสารที่ไม่เหมือนกับการออกแบบเลย จนกระทั่งความสามารถนี้มีอยู่ คอนเทนเนอร์สองมิติเพียงแบบเดียวที่เอนจินวัดขนาดได้คือตาราง ดังนั้นเลย์เอาต์การ์ดทุกแบบจึงต้องถูกเขียนใหม่เป็นตารางด้วยมือ
อะไรเปลี่ยนไปในแบบจำลองเลย์เอาต์
ลูปหลักเดิมรักษากล่องบรรทัดเดียวและเดินหน้าลงไปตามหน้า แบบจำลองนั้นจัดการเนื้อหาแบบอินไลน์และบล็อกซ้อนกันได้อย่างสมบูรณ์แบบ แต่ไม่สามารถแสดงคอนเทนเนอร์ที่ลูกของมันมีขนาดสัมพันธ์กันได้ ตารางเป็นข้อยกเว้นเพียงอย่างเดียว ซึ่งมีการวัดขนาดแบบสองรอบเป็นของตัวเอง
flex และ grid แต่ละอย่างเพิ่มรอบการวัดขนาดที่มีขอบเขตจำกัดเหนือลูกของคอนเทนเนอร์ และคำสำคัญคือ "มีขอบเขตจำกัด" คอนเทนเนอร์ flex วัดขนาดลูกโดยตรงได้สูงสุด 256 ตัวลงในอาร์เรย์ขนาดคงที่ grid ใช้เมทริกซ์การครอบครองขนาดไม่เกิน 64 คูณ 64 เซลล์ สำหรับการวางตำแหน่งอัตโนมัติที่กำหนดผลลัพธ์ได้แน่นอน เพดานเหล่านี้มีอยู่เพื่อไม่ให้สไตล์ชีตที่ประสงค์ร้ายหรือสร้างขึ้นมาสามารถขับเคลื่อนการเรียกซ้ำแบบไม่มีขอบเขตหรือหน่วยความจำการวางตำแหน่งแบบกำลังสองได้ ซึ่งเป็นข้อกังวลจริงเมื่อ HTML มาจากเทมเพลตที่ลูกค้าแก้ไขเอง
รายการ flex ได้รับขนาดของตัวเองอย่างไร
ในทิศทางแถว คอนเทนเนอร์จะรวมค่าฐานของแต่ละรายการเข้ากับน้ำหนักการขยายและการหดตัวของมัน แล้วจึงกระจายพื้นที่ที่เหลืออยู่ ไม่ว่าจะเป็นบวกหรือลบ ตามน้ำหนักเหล่านั้น เมื่อใช้ flex-wrap แต่ละบรรทัดจะถูกแก้ไขอย่างอิสระต่อกัน ดังนั้นแถวที่แตกออกเป็นสองบรรทัดจะกำหนดพื้นที่ว่างต่อบรรทัด แทนที่จะกำหนดข้ามทั้งคอนเทนเนอร์ ในทิศทางคอลัมน์ การกระจายตามแกนหลักแบบเดียวกันนี้จะทำงานเทียบกับความสูงที่ระบุไว้อย่างชัดเจนหรือความสูงของเนื้อหา
justify-content, align-items, gap และทิศทางย้อนกลับทำงานบนเรขาคณิตที่วัดขนาดไว้แล้ว มันย้ายกล่อง แต่ไม่เคยกระตุ้นการวัดขนาดเนื้อหาของรายการซ้ำ การแยกส่วนนี้เองที่ป้องกันไม่ให้แดชบอร์ดที่ซับซ้อนวัดขนาดลูกของมันซ้ำหลายครั้ง
uses
PDFlibrary;
var
Lib: TPDFlib;
Html, Remainder: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.SetPageSize('A4');
Lib.NewPage;
Html :=
'<div style="display:flex; gap:12px;">' +
' <div style="flex:2 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Revenue</b><br/>EUR 4,182,300</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Margin</b><br/>18.4%</div>' +
' <div style="flex:1 1 0; background:#f4f6f8; padding:8px;">' +
' <b>Backlog</b><br/>92 days</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 40, 515, 120, Html);
if Remainder <> '' then
Log('content did not fit - carry the remainder to the next box');
Lib.SaveToFile('dashboard.pdf');
finally
Lib.Free;
end;
end;
ค่าที่คืนกลับมาคือสตริงการต่อเนื่อง ซึ่งเป็นวิธีที่จุดเข้าถึงการวาด HTML ทุกจุดรายงานสิ่งที่ไม่พอดี ส่งมันไปยังกล่องถัดไปหรือหน้าถัดไป แล้วการไหลจะดำเนินต่อจากจุดที่หยุดไว้
การวางตำแหน่งกริด และแทร็กสามารถเป็นอะไรได้บ้าง
แทร็กกริดรับความยาวคงที่ เปอร์เซ็นต์ หน่วย fr นิพจน์ repeat() แบบง่าย และ minmax() การวางตำแหน่งอัตโนมัติเติมเมทริกซ์การครอบครองอย่างกำหนดผลลัพธ์ได้แน่นอน ดังนั้น HTML เดียวกันจึงให้การจัดเรียงเดียวกันเสมอ พิกัดที่ระบุอย่างชัดเจนได้รับอนุญาตให้ซ้อนทับกันได้ ซึ่งเป็นการจงใจ การออกแบบที่ซ้อนป้ายทับการ์ดกำลังแสดงความตั้งใจ ไม่ใช่ข้อผิดพลาด เมื่อระบุเพียงแกนเดียวอย่างชัดเจน การวางตำแหน่งจะค้นหาเฉพาะแกนที่เหลือเท่านั้น
รายการที่ครอบคลุมหลายแถวจะส่งความสูงที่วัดได้กลับไปยังแถวที่มันครอบคลุม โดยเฉลี่ยไว้ในแถวเหล่านั้น ซึ่งป้องกันไม่ให้รายการที่ครอบคลุมสูงบีบแถวเดียวจนแคบในขณะที่ทิ้งแถวข้างเคียงไว้สั้น:
Html :=
'<div style="display:grid; grid-template-columns:repeat(3, 1fr); ' +
' gap:10px;">' +
' <div style="grid-row:span 2; background:#eef;">Site plan</div>' +
' <div>Inspector</div>' +
' <div>Date</div>' +
' <div style="grid-column:2 / span 2;">Findings summary</div>' +
'</div>';
Remainder := Lib.DrawHTMLTextBox(40, 180, 515, 260, Html);
ลูกของ flex และ grid ถูกเรนเดอร์ผ่านตัวเรนเดอร์ HTML เดียวกันกับสิ่งอื่นทั้งหมด ซึ่งเป็นคุณสมบัติที่ทำให้ฟีเจอร์นี้ใช้งานได้จริง แทนที่จะเป็นโลกแยกต่างหาก ฟอนต์ การซ้อนทับของ CSS ลิงก์ ภาพ ตาราง และคอนเทนเนอร์ flex หรือ grid ที่ซ้อนกันอีกชั้น ล้วนมีพฤติกรรมภายในรายการ flex เหมือนกับที่มันมีในระดับบนสุดทุกประการ และแผนเลย์เอาต์ภายนอกบันทึกคำสั่งข้อความและสี่เหลี่ยมสุดท้ายไว้ ทำให้การวาดซ้ำใช้แคชการวัดขนาดที่มีอยู่แล้วซ้ำได้
เหตุใดเชิงอรรถจึงเป็นปัญหาการแบ่งหน้า
เชิงอรรถไม่ใช่เนื้อหาที่ไหลตามหลังย่อหน้าที่มีการอ้างอิงถึงมัน แต่เป็นเนื้อหาที่ต้องปรากฏที่ท้ายกล่องเดียวกันกับการอ้างอิงของมัน นั่นกลับลำดับการวัดขนาดตามปกติ เพราะพื้นที่ที่มีให้สำหรับข้อความเนื้อหาตอนนี้ขึ้นอยู่กับเนื้อหาที่ยังไม่ถูกจัดเลย์เอาต์เลย
ตัวเรนเดอร์จึงวัดขนาดเชิงอรรถเมื่อพบการอ้างอิง และหักพื้นที่เชิงอรรถออกจากงบประมาณความสูงเนื้อหาของกล่องที่มีขอบเขตปัจจุบัน หากการอ้างอิง ข้อความเนื้อหาที่มีอยู่ และเชิงอรรถ ไม่สามารถใส่พอดีได้ทั้งหมด เครื่องหมายเชิงอรรถและทุกอย่างหลังจากนั้นจะย้ายเข้าไปในสตริงการต่อเนื่องพร้อมกัน กฎนี้เองที่ป้องกันความล้มเหลวคลาสสิกสองแบบ คือเชิงอรรถพิมพ์ทับข้อความเนื้อหา และเชิงอรรถติดค้างอยู่บนหน้าที่การอ้างอิงของมันอยู่บนหน้าก่อนหน้า
ในกล่องที่มีขอบเขต พื้นที่เชิงอรรถถูกตรึงไว้ที่ด้านล่างพร้อมเส้นคั่นอยู่เหนือมัน ในการวัดขนาดแบบไม่มีขอบเขต ซึ่งไม่มีความสูงกล่องให้ตรึงไว้ พื้นที่เชิงอรรถจะตามหลังเนื้อหาทันที การนับเลขถูกพกไว้ในฟิลด์ส่วนขยายบนสแตกการต่อเนื่อง ดังนั้น DrawHTMLTextBox และ DrawHTMLStory จึงรักษาลำดับให้ดำเนินต่อไปข้ามคอลัมน์และหน้า และสตริงการต่อเนื่องที่สร้างขึ้นก่อนที่ฟิลด์นั้นจะมีอยู่ก็ยังคงดำเนินต่อได้อย่างถูกต้อง
// เชิงอรรถภายในเรื่องแบบหลายคอลัมน์รักษาลำดับที่ดำเนินต่อเนื่องเดียว
Html := LoadTemplate('chapter.html'); // ใช้เครื่องหมาย float:footnote
Remainder := Lib.DrawHTMLStory(40, 40, 515, 700,
2, // คอลัมน์
16, // ร่องกลางเป็นหน่วยพอยต์
20, // จำนวนหน้าสูงสุดสำหรับเรื่องนี้
Html);
if Remainder <> '' then
Log('story exceeded its page budget');
คำแนะนำเชิงปฏิบัติสำหรับผู้เขียนเทมเพลต
ออกแบบภายในเพดานที่บันทึกไว้ คอนเทนเนอร์ flex ที่มีลูกโดยตรงมากกว่า 256 ตัวมักจะเป็นตารางข้อมูลที่สวมชุด flex อยู่เกือบทุกครั้ง และเส้นทางตารางก็วัดขนาดมันได้ดีกว่าอยู่ดี grid ที่ใหญ่กว่า 64 คูณ 64 คือสเปรดชีต และคำแนะนำเดียวกันนี้ใช้ได้เช่นกัน สำหรับข้อความเนื้อหาแบบหลายคอลัมน์ พฤติกรรมคอลัมน์และการแบ่งพยางค์ที่อธิบายไว้ในการแบ่งพยางค์และคอลัมน์ข้อความแบบสมดุล ควบคุมว่าการไหลมีหน้าตาอย่างไรภายในแต่ละคอลัมน์
วัดขนาดก่อนวาดเมื่อเลย์เอาต์ต้องพอดี GetHTMLTextHeight รายงานความสูงที่ความกว้างที่กำหนดจะต้องการ ซึ่งเป็นวิธีที่ประหยัดในการตัดสินใจระหว่างเลย์เอาต์แบบหนึ่งกับอีกแบบก่อนที่จะลงมือจริง และปฏิบัติต่อสตริงการต่อเนื่องที่ไม่ว่างเปล่าว่าเป็นเรื่องปกติ ไม่ใช่ข้อยกเว้น มันคือกลไกที่เนื้อหายาวใช้แบ่งหน้า ไม่ใช่สัญญาณข้อผิดพลาด
เมื่อ HTML มาจากเอนจินสร้างรายงานแทนที่จะมาจากเทมเพลตที่เขียนด้วยมือ เส้นทางที่ขับเคลื่อนด้วยชุดข้อมูลในเอนจินสร้างรายงานจากชุดข้อมูล ประกอบเข้ากันได้ดีกับสิ่งนี้ โดยสร้างมาร์กอัปที่ flex และ grid นำไปจัดเรียงต่อ และเมื่อเนื้อหาเดียวกันต้องออกจาก PDF อีกครั้งด้วย เส้นทางการส่งออกเชิงความหมายในการส่งออก PDF เป็น Markdown และ DOCX จะปิดวงจรการไปกลับให้สมบูรณ์
เลย์เอาต์ HTML การสร้างรายงาน และการส่งออกเชิงความหมาย เป็นส่วนหนึ่งของไลบรารีเดียวกันสำหรับ Delphi, C++Builder และ Free Pascal รายการคุณสมบัติทั้งหมดอยู่ที่หน้า PDF Library for Delphi