บทความเทคนิค

Resolution ของ HotPDF ใน Delphi: หน่วยวาดกับ UserWidth

ใน HotPDF Component THotPDF.Resolution นิยามหน่วยการวาด: พิกัด X กับ Y ทุกตัว, margin ทุกตัว, ขนาดที่ส่งให้ SetFont และผลของ TextWidth กับ GetWideTextWidth ล้วนวัดเป็น 1/Resolution นิ้ว THPDFPage.Width กับ Height ไม่ฟังมันและยังอยู่ในพอยต์ ขอบเขต layout จึงต้องมาจาก UserWidth กับ UserHeight ที่อ่านอย่างเดียว เหตุผลที่ต้องแตะ Resolution ปกติคือการ port: engine รายงานที่คิดเป็น 1/96 หรือ 1/144 นิ้วอยู่แล้วจะย้ายมาง่ายกว่าเมื่อฝั่ง PDF พูดหน่วยเดียวกัน กว่าที่ทุกจุดเรียกต้องแบกตัวคูณแปลงหน่วย วิธีนี้ใช้ได้ดี ตราบใดที่คุณรู้ว่าตัวเลขไหนย้ายไปอยู่หน่วยใหม่และตัวไหนยังค้างอยู่ที่เดิม

THotPDF.Resolution เปลี่ยนอะไรจริง ๆ

THotPDF.Resolution เปลี่ยนเฉพาะว่า HotPDF อ่านตัวเลขที่คุณส่งให้อย่างไร ของที่มันเขียนออกไปเหมือนเดิม setter เป็นสองบรรทัด: SetResolution เก็บค่าไว้แล้วตั้ง DocScale := Value / 72 จากจุดนั้น XProjection กับ YProjection หารพิกัดทุกตัวด้วย DocScale ระหว่างทางเข้า content stream และ SetFont หารขนาดแบบเดียวกันก่อนบันทึก user space ของ PDF เริ่มต้นที่ 1/72 นิ้ว (ISO 32000-1 §8.3.2.3) ที่ Resolution เริ่มต้น 72 การฉายจึงเป็น identity และที่ 144 หนึ่งหน่วยวาดคือครึ่งพอยต์ ไม่มี entry ของ /UserUnit ถูกเขียน attribute ของหน้าที่เพิ่มใน PDF 1.6 นั้นเป็นเรื่องแยกต่างหากที่ HotPDF เปิดเป็น THPDFPage.SetUserUnit รายละเอียดหนึ่งที่อาละวนคนที่มาจากบทเรียน TextOut: พิกัดหน้าเริ่มจากมุมบนซ้ายโดย Y วิ่งลงล่าง เพราะ YProjection คำนวณเป็นยอดของ MediaBox ลบด้วย Y ที่สเกลแล้ว และข้อนี้เป็นจริงทุกค่า Resolution

THotPDF.Resolution นิยามหน่วยการวาดใน Delphi อย่างไร: setter เก็บ DocScale เป็น Resolution หารด้วย 72 แล้ว XProjection, YProjection กับ SetFont หารพิกัดกับขนาดทุกตัวระหว่างทางเข้า content stream Resolution 72 จึงเป็นการฉายแบบ identity และ Resolution 144 ทำให้หนึ่งหน่วยวาดเป็นครึ่งพอยต์ ขณะที่หน้ายังเริ่มบนซ้ายโดย Y วิ่งลงล่างเหมือนเดิม
ไม่มีอะไรในไฟล์ output ขยับ — มีแต่ความหมายของตัวเลขที่คุณส่งเข้าไปเปลี่ยน เหตุผลที่ content stream เดียวกันโผล่ได้ทั้งที่ 72 และ 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1 หน่วยวาด = 1/144 นิ้ว
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // หนึ่งนิ้วในหน่วยวาด
    Page.SetFont('Arial', [fsBold], 28); // 28/144 นิ้ว ฟอนต์ 14 พอยต์
    Title := 'INVOICE 2026-0417';
    // ชิดขวากับขอบหน้าที่วัดในหน่วยเดียวกัน
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // เส้นหนา 1 พอยต์
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

ทำไม Page.Width ไม่ตรงกับพิกัดของคุณที่ Resolution 144

THPDFPage.Width กับ Height รายงานหน้าเป็นพอยต์ไม่ว่า Resolution ของเอกสารจะเป็นอะไร ขณะที่พิกัดของคุณเป็น 1/Resolution นิ้ว ที่ 144 หน้าจึงดูกว้างเป็นครึ่งของความจริง หน้า A4 อ่านได้ Width = 595 กับ Height = 842 ที่ Resolution 72 และยังอ่านได้ 595 กับ 842 ที่ 144 ทั้งที่ขอบขวาอยู่ที่ X = 1190 จริง ๆ UserWidth กับ UserHeight ที่เพิ่มใน v2.766.0 คืน Width * DocScale ซึ่งคือขนาดหน้าในหน่วยที่คุณกำลังวาด ก่อนที่สองตัวนี้จะมี library ปนสองอย่างกันเองข้างใน อาการที่ Resolution 144 จึงดุดัน: ย่อหน้าตัดคำทุกตัวอักษร THPDFTable.Render ผลักทุกแถวลงหน้าใหม่ และทั้งตัวนำเข้า HTML กับตัว flatten ฝั่ง XFA วาดเนื้อหาขนาดครึ่งเดียว ฟอร์มที่ flatten แล้วเบียดกองอยู่มุมบนซ้าย layout ของย่อหน้า การ render ตาราง การนำเข้า HTML การจัดกึ่งกลาง EMF, clip หน้าฝั่ง WMF และการวินิจฉัย layout ตอนนี้อ่านขนาดหน่วยผู้ใช้ทั้งหมด โค้ด layout ของคุณก็ควรทำตาม: สิ่งใดที่เทียบกับพิกัดการวาด (ขอบขวา, เทสต์ขึ้นหน้าใหม่, การคำนวณจัดกึ่งกลาง) ต้องอยู่บน UserWidth กับ UserHeight ไม่ใช่บน Width กับ Height เด็ดขาด

กับดักหนึ่ง: กำหนดค่า Width หรือ Height แล้วหน้าสลับไปเป็นพอยต์

การกำหนด Page.Width หรือ Page.Height เปลี่ยนหน้าไปเป็น UserDefined เงียบ ๆ และหน้าแบบ UserDefined เมิน DocScale ทั้งตัว ทุกอย่างที่คุณวาดต่อจากนั้นจึงอยู่ในพอยต์ ไม่ใช่ 1/Resolution นิ้ว setter ตัวนี้เก่าและรับพอยต์เป็นการออกแบบ ความหมายของมันจึงถูกปล่อยไว้ตามเดิม การฉายของหน้า UserDefined เป็น X + MinX เปล่า ๆ และ SetFont เก็บขนาดไว้ตามที่ได้มา ที่ Resolution 144 ผลลัพธ์คือหน้าที่เนื้อหาพุ่งออกมาใหญ่เป็นสองเท่าของหน้าก่อนหน้าทันที library เคยทำความผิดพลาดนี้กับตัวเองพอดี ๆ: หน้าต่อเนื่องของย่อหน้าเคยคัดลอกขนาดของหน้าก่อนหน้าผ่าน Width และทุกหน้า overflow เปลี่ยนไปใช้พอยต์หมด หน้าพวกนี้ตอนนี้คัดลอก Size, Orientation และ Resolution ของหน้าแทน แล้ว fallback ไป Width กับ Height เฉพาะเมื่อหน้าต้นทางเป็น UserDefined อยู่แล้ว

มีสองทางออกตามสิ่งที่ต้องการ ถ้ากระดาษมาตรฐานพอ ตั้ง Page.Size กับ Page.Orientation แล้ววาดต่อในหน่วย Resolution ของคุณ ถ้าจำเป็นต้องมีขนาดหน้ากำหนดเองจริง ๆ ยอมรับเถอะว่ามันเป็นหน้าพอยต์แล้ววาดเป็นพอยต์ UserWidth จะเท่ากับ Width ตรงนั้น โค้ด layout ที่อ่านแต่ UserWidth จึงยังทำงานได้ทั้งสองชนิดหน้า ชุดทดสอบตอกตะปูไว้แบบนี้: ที่ Resolution 144 หน้า A4 รายงาน UserWidth เป็น 1190 แต่หลัง Width := 500 กับ Height := 400 มันรายงาน 500 กับ 400 หน้าที่โหลดมาทำตัวเหมือนกัน เพราะหน้าที่สร้างใหม่จาก PDF ที่มีอยู่รู้จักแค่ MediaBox เป็นพอยต์แล้ววาดเป็นพอยต์ หน้าที่เอกสารนี้สร้างเองคงหน่วยของตัวเองไว้เมื่อคุณสลับไปหน้าอื่นแล้วกลับมาผ่าน CurrentPageNumber ซึ่งเป็นแบบนี้มาตั้งแต่ v2.766.26

ทำไม Page.Width เห็นต่างกับพิกัดของคุณที่ Resolution 144 ใน HotPDF: Width กับ Height ยังอยู่ในพอยต์ขณะที่การวาดใช้ 1/144 นิ้ว หน้า A4 จึงอ่านได้ 595 แต่ขอบขวาจริงอยู่ที่ UserWidth 1190 และการกำหนดค่า Width เปลี่ยนหน้าไปเป็น UserDefined ที่เมิน DocScale ย่อหน้าจึงตัดคำทุกตัวอักษร ตารางแตกทุกแถวและขนาดของ SetFont เหลือครึ่ง
สิ่งใดที่เทียบกับพิกัดการวาดต้องอยู่บน UserWidth กับ UserHeight — บนหน้าพอยต์แบบ UserDefined สองตัวนี้ทับกัน โค้ด layout ชุดเดียวจึงรอดทั้งสองกรณี

กับดักสอง: ทำไมขนาดฟอนต์ออกมาครึ่งเดียว

ขนาดฟอนต์ที่เริ่มเป็นพอยต์จะออกมาครึ่งขนาดที่ Resolution 144 เพราะ SetFont ถือว่า argument ขนาดเป็นหน่วยวาดแล้วแปลงเป็นพอยต์ก่อนเก็บ ภายใน SetFont เก็บ ASize / DocScale * DPI ลงใน object ฟอนต์ปัจจุบัน ค่าที่เก็บจึงเป็นพอยต์เสมอ library สะดุดจุดนี้สองครั้ง: font fallback ใน WideTextOutBoxEx กับหน้าต่อเนื่องของย่อหน้า ต่างส่งค่าพอยต์ที่เก็บไว้กลับเข้า SetFont ซึ่งสเกลมันรอบที่สองแล้วตัดข้อความให้เหลือครึ่ง โค้ดของคุณอ่านค่าที่เก็บไว้ไม่ได้ แต่บั๊กแบบเดียวกันโผล่ทุกครั้งที่ค่าพอยต์จากที่อื่นไหลเข้า SetFont: TFont.Size จากฟอร์ม VCL, ขนาดในนิยามรายงาน หรือความยาว pt จาก CSS แปลงมันก่อนเสมอ แล้วใส่ Resolution ของหน้าเองกับกรณี UserDefined ลงในตัวคูณด้วย เหมือนที่ playback ของ metafile ทำตอนรีเลย์ Canvas ของหน้า (ดูวิธีที่ HotPDF นำเข้ากราฟิกเวกเตอร์ EMF กับ WMFสำหรับเส้นทางนั้น):

// หน่วยวาดต่อหนึ่งพอยต์บนหน้าปัจจุบัน ทำกระจกตามการฉาย
// ที่ HotPDF ใช้: 1 บนหน้าที่กำหนดขนาดผ่าน Width/Height ไม่งั้น
// (document Resolution / 72) * (page Resolution / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // TFont.Size เป็นพอยต์; SetFont คาดหวังหน่วยวาด
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

library ใช้กฎเดียวกันกับค่าคงที่พอยต์ของตัวเอง ฟอนต์ 12 พอยต์ที่ทุกหน้าใหม่เริ่มด้วยตอนนี้ถูกคูณด้วยตัวประกอบ units-per-point ภายใน จึงเป็น 12 พอยต์ทุกค่า Resolution DrawChart ที่ margin, ขนาด label และความกว้างเส้นล้วนตายตัวเป็นพอยต์ ตอนนี้รันโดยตั้งสเกลชั่วคราวเป็น 1 สิ่งที่ยังอยู่ในหน่วยวาดโดยตั้งใจ คือค่าเริ่มต้นของพารามิเตอร์สาธารณะอย่างขนาดโมดูลของ DrawQRCode กับขนาดฟอนต์เริ่มต้นของตาราง พวกมันเป็นส่วนหนึ่งของสัญญา API ที่ Resolution 144 พวกมันจึงหมายถึงครึ่งหนึ่งของที่ 72 ถ้าคุณจัดขนาดรายงานจาก template คู่มือการเอาต์พุตรายงานด้วยฟอนต์และภาพใน HotPDFเล่าว่าค่าพวกนั้นมักมาจากไหน

ตรวจยังไงว่า layout เป็นอิสระจาก Resolution

เช็คที่เชื่อถือได้ที่สุดคือการเทียบไบต์: render หน้าเดิมที่ Resolution 72 แล้ว render อีกรอบที่ 144 โดยคูณพิกัดกับขนาดทุกตัวเป็นสองเท่า สตรีมเนื้อหาที่ยังไม่บีบอัดต้องเหมือนกัน ทั้งสองรอบตกลงที่ค่าพอยต์เดียวกันหลังการฉาย ความต่างใด ๆ จึงคือค่าที่หลุดพ้นการแปลงหน่วย ชุดทดสอบของ HotPDF เช็คย่อหน้า ตาราง การนำเข้า HTML, การ flatten ฝั่ง XFA, ส่วนโค้ง, metafile และภาพด้วยวิธีนี้ เทคนิคเดียวกันใช้กับโค้ดรายงานของคุณได้ด้วย harness แทบเป็นศูนย์:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // สตรีมเนื้อหาที่อ่านออก
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// RenderPage('r72.pdf', 72, 1) and RenderPage('r144.pdf', 144, 2)
// ต้องได้สตรีมเนื้อหาของหน้าที่เหมือนกันทุกไบต์
วิธีตรวจว่าโค้ด Delphi ของ HotPDF เป็นอิสระจาก Resolution: render layout เดิมสองรอบ หนึ่งรอบที่ Resolution 72 สเกล 1 หนึ่งรอบที่ 144 โดยคูณพิกัดกับขนาดฟอนต์ทุกตัวเป็นสองเท่า แล้วบังคับให้สตรีมเนื้อหาที่ยังไม่บีบอัดเหมือนกันทุกไบต์ — ความเพี้ยนชี้ไปที่หน้าที่ถูกเปลี่ยนเป็น UserDefined ผ่าน Width หรือค่าพอยต์ที่ไม่ได้แปลงหน่วยไหลเข้า SetFont
ทั้งสองรอบตกลงที่ค่าพอยต์เดียวกันหลังการฉาย ความต่างใด ๆ จึงคือตัวเลขที่หลุดการแปลงหน่วย — harness ชุดเดียวกับที่ชุดทดสอบของ HotPDF พึ่งพา

ดู operator ที่แบกตัวเลขไว้: Td, Tm, Tf, re, w กับ array ของ TJ ไบต์ระดับไฟล์ยังต่างกันได้ที่วันสร้างกับ /ID จึงให้เทียบสตรีม ไม่ใช่ทั้งไฟล์ ความเพี้ยนเกือบทุกครั้งชี้ไปที่กับดักสองข้อข้างบนอย่างใดอย่างหนึ่ง: หน้าที่ถูกเปลี่ยนขนาดผ่าน Width หรือค่าพอยต์ที่ถูกส่งตรงเข้า SetFont ถ้าคุณยังใหม่กับการเรียกวาดเอง เริ่มที่บทเรียน TextOut ของ HotPDF เรื่องขนาด สไตล์ และการหมุนแล้วค่อยกลับมาสลับ Resolution เมื่อ layout ของคุณอ่าน UserWidth รายละเอียด API เต็มกับดาวน์โหลดทดลองอยู่ที่หน้า HotPDF Delphi PDF component