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

ส่งออกหน้า PDF เป็น SVG ใน Delphi ด้วย HotPDF

HotPDF ส่งออกหน้าใดก็ได้ของเอกสาร PDF ที่โหลดไว้เป็นมาร์กอัป SVG แบบสแตนด์อโลนด้วยการเรียกเพียงครั้งเดียวคือ BuildLoadedPageSVG ซึ่งคืนค่าเอกสาร SVG ฉบับสมบูรณ์ในรูปแบบสตริง มาร์กอัปที่ส่งออกมาประกอบด้วยเรขาคณิตของหน้า ข้อความในรูปอิลิเมนต์ SVG text จริง ภาพแรสเตอร์ที่ฝังไว้ และสถานะเส้นขอบที่ตัวดำเนินการ PDF กำหนดไว้ในแต่ละขั้นตอนการวาด

ส่วนหลังนี้เองที่ทำให้ตัวแปลงไฟล์ที่พัฒนาขึ้นเองส่วนใหญ่พังลงอย่างเงียบๆ การแปลงหน้า PDF เป็น SVG ดูเหมือนจะเป็นปัญหาเรื่องพิกัด แต่แท้จริงแล้วกลับเป็นปัญหาเรื่องสถานะ PDF เป็นเครื่องสแตกที่สถานะกราฟิกเปลี่ยนแปลงไปตามการตีความสตรีมเนื้อหา ส่วน SVG เป็นโครงสร้างต้นไม้เชิงประกาศที่แต่ละอิลิเมนต์พกแอตทริบิวต์การแสดงผลของตัวเองมาด้วย สิ่งใดก็ตามที่ตัวแปลความหมายไม่ได้บันทึกสถานะไว้ในขณะที่สร้างอิลิเมนต์นั้นจะหายไปจากผลลัพธ์โดยสิ้นเชิง และความผิดพลาดนี้เกิดขึ้นอย่างเงียบๆ คุณจะได้ SVG ที่ถูกต้องตามรูปแบบแต่แสดงผลหน้าที่ผิดเพี้ยนไปเล็กน้อย

เหตุใดหน้า PDF จึงไม่สามารถแปลงเป็น SVG ได้โดยตรง

ความไม่ตรงกันสามประการทำให้การแปลงไม่ใช่เรื่องง่าย และทั้งสามอย่างล้วนสร้างผลลัพธ์ที่ดูสมเหตุสมผลจนกว่าคุณจะนำไปเทียบเคียงกับต้นฉบับ ประการแรกคือแกน y พื้นที่ผู้ใช้ของ PDF ขยายขึ้นจากมุมล่างซ้ายของหน้า ส่วน SVG ขยายลงจากมุมบนซ้าย การพลิกทั้งหน้าเพียงครั้งเดียวจะแก้ไขพิกัดการวาดได้ก็จริง แต่กลับทำให้ทุกกลิฟเสียหาย เพราะการพลิกทั้งแคนวาสยังทำให้รูปทรงตัวอักษรกลับด้านไปด้วย

ความไม่ตรงกันประการที่สองคือการสืบทอดคุณสมบัติ ใน PDF คำสั่ง q และ Q จะพุชและป็อปสถานะกราฟิกที่ประกอบด้วยความกว้างเส้น รูปแบบปลายเส้น รูปแบบมุมต่อเส้น ค่าจำกัดมุมแหลม อาร์เรย์เส้นประ เฟสเส้นประ และค่าอัลฟา ใน SVG อิลิเมนต์ที่ไม่ได้ระบุแอตทริบิวต์ใดไว้จะสืบทอดค่านั้นมาจากกลุ่มบรรพบุรุษ ซึ่งเป็นกฎขอบเขตที่ต่างออกไปโดยสิ้นเชิง ตัวส่งออกที่ติดตามเฉพาะเมทริกซ์การแปลงปัจจุบันแต่ลืมสถานะเส้นขอบจะปล่อยให้สถานะที่ถูกเรียกคืนหลังคำสั่ง Q รั่วไหลเข้าไปในอิลิเมนต์ที่ตามมา

ประการที่สามคือ PDF แสดงหลายสิ่งด้วยข้อตกลงมากกว่าด้วยค่าจริง รูปแบบปลายเส้นและมุมต่อเส้นเป็นจำนวนเต็ม ความกว้างเส้นเป็นศูนย์หมายถึงเส้นบางระดับพื้นที่อุปกรณ์ ไม่ใช่เส้นที่มองไม่เห็น และตัวดำเนินการวาดรุ่นที่มีเครื่องหมายดอกจันจะเปลี่ยนกฎการพันเส้นแทนที่จะเปลี่ยนสี แต่ละสิ่งเหล่านี้ต้องการการแปลความหมาย ไม่ใช่การคัดลอกตรงๆ

การเรียกเพียงครั้งเดียวสำหรับกรณีใช้งานทั่วไป

สำหรับงานทั่วไปอย่างการส่งออกหน้าเพื่อใช้กับตัวแสดงผลบนเว็บ เครื่องมือเทียบความต่าง หรือการส่งมอบงานออกแบบ พื้นผิว API มีเพียงฟังก์ชันเดียว BuildLoadedPageSVG รับดัชนีหน้าแบบเริ่มจากศูนย์เทียบกับเอกสารที่โหลดอยู่ในปัจจุบัน และคืนค่าเอกสาร SVG เป็น AnsiString:

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile คืนค่าจำนวนหน้า
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      Svg := Pdf.BuildLoadedPageSVG(I);
      if Length(Svg) = 0 then
        Continue;
      Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
      try
        Output.WriteBuffer(Svg[1], Length(Svg));
      finally
        Output.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

การส่งออกแบบเดียวกันนี้ยังเปิดให้ใช้งานผ่านเครื่องมือบรรทัดคำสั่งของ HotPDF ในรูปคำสั่ง export-svg ซึ่งมีประโยชน์ในไปป์ไลน์การบิลด์และสคริปต์การทดสอบถดถอยที่ต้องการรูปแบบข้อความของหน้าที่เทียบความต่างได้โดยไม่ต้องเขียนโค้ด Pascal เลย เนื่องจาก SVG เป็นข้อความ จึงเป็นคู่หูตามธรรมชาติของเส้นทางแรสเตอร์ที่อธิบายไว้ในการเรนเดอร์หน้า PDF เป็นบิตแมป บิตแมปบอกคุณว่าหน้าตาของหน้านั้นเป็นอย่างไร ส่วน SVG บอกคุณว่าหน้านั้นประกอบขึ้นจากอะไร

PDF แม็ปข้อความไปยังอิลิเมนต์ข้อความของ SVG ได้อย่างไร

HotPDF ประกอบสายเมทริกซ์ข้อความในลำดับ prefix คูณด้วย CTM คูณด้วยเมทริกซ์ข้อความ คูณด้วยการพลิกกลิฟ โดยการพลิกกลิฟคือการคูณทางขวาด้วย matrix(1,0,0,-1,0,0) ตัวประกอบทางขวานี้มีไว้เพื่อหักล้างการพลิกแนวตั้งระดับหน้าสำหรับรูปทรงกลิฟเท่านั้น เพราะข้อความ SVG ที่วาดในกรอบท้องถิ่นที่ถูกพลิกจะปรากฏกลับหัวหากไม่มีตัวประกอบนี้ การใส่การแก้ไขไว้ในเมทริกซ์แทนที่จะเขียนเป็นโค้ดกรณีพิเศษ ทำให้ข้อความที่หมุน กลับด้าน และเฉือนมุม ล้วนออกมาถูกต้องโดยไม่ต้องเพิ่มเงื่อนไขแยก

การจัดตำแหน่งแนวนอนใช้ไวยากรณ์ x แบบหลายค่าของอิลิเมนต์ SVG text โดยมีหนึ่งพิกัดต่อหนึ่งตัวอักษร สะสมจากระยะเลื่อนของแต่ละกลิฟบวกกับระยะห่างตัวอักษร Tc และระยะห่างคำ Tw ที่มีผลอยู่ในขณะนั้น การขยายแนวนอน Tz จะถูกพับรวมเข้ากับคอลัมน์ a และ c ของเมทริกซ์ข้อความแทนที่จะส่งออกแยกต่างหาก ทำให้ตัวแสดงผลที่ไม่รองรับแอตทริบิวต์ข้อความพิเศษยังคงวางกลิฟทุกตัวไว้ตรงตำแหน่งที่ PDF กำหนด ข้อความที่สร้างผ่านการเรียงรูปที่ซับซ้อน ตามที่อธิบายไว้ในการเรียงรูปข้อความสำหรับสคริปต์ที่ซับซ้อน ก็เดินตามเส้นทางเดียวกันนี้ เพราะตัวเรียงรูปได้แปลงกลุ่มอักขระเป็นกลิฟที่จัดตำแหน่งแล้วตั้งแต่ก่อนที่สตรีมเนื้อหาจะถูกตีความ

การหมุนและภาพ: การพลิกสองแบบที่ทำผิดทิศทางได้ง่าย

หน้าที่มีรายการ /Rotate ไม่เท่ากับศูนย์ต้องการการแปลงล่วงหน้าที่ประกอบด้วยการพลิกเทียบกับความสูงแคนวาสที่หมุนแล้ว และการหมุนที่แสดงในพื้นที่แสดงผลแบบ y ขึ้น เมทริกซ์การหมุนทั้งสามคือ (0,-1,1,0,0,W) สำหรับ 90 องศา (-1,0,0,-1,W,H) สำหรับ 180 องศา และ (0,1,-1,0,H,0) สำหรับ 270 องศา โดย W และ H คือขนาดหน้าก่อนการหมุน การคำนวณค่าเหล่านี้ด้วยมือมักนำไปสู่ข้อผิดพลาดด้านเครื่องหมายในสามจุดพอดี ตัวส่งออกจึงประกอบเมทริกซ์เหล่านี้ผ่านรูทีนการคูณเมทริกซ์เดียวกับที่จัดการการแปลงอื่นๆ ทั้งหมด

ภาพที่ฝังไว้ต้องการการพลิกของตัวเอง เพราะพื้นที่ภาพของ PDF วางแถวตัวอย่างแรกไว้ที่ขอบบนของสี่เหลี่ยมหน่วย ในขณะที่อิลิเมนต์ SVG image ใช้กรอบท้องถิ่นแบบ y ลง การแปลงที่ส่งออกจึงเป็น CTM คูณทางขวาด้วย matrix(1,0,0,-1,0,1) หากทำผิดพลาดตรงนี้จะทำให้ภาพถ่ายกลับด้านในแนวตั้งบนหน้าที่ถูกต้องสมบูรณ์ทุกอย่างยกเว้นจุดนี้ ซึ่งเป็นข้อบกพร่องแบบที่ผู้ตรวจสอบจะสังเกตเห็นได้ทันที แต่การทดสอบอัตโนมัติมักตรวจไม่พบ

อุปกรณ์สถานะกราฟิกเก็บรักษาอะไรไว้บ้างจริงๆ

HotPDF ส่งต่อตัวดำเนินการสถานะเส้นขอบ w, J, j, M และ d ผ่านอินเทอร์เฟซอุปกรณ์เสริมแยกต่างหาก ทำให้เพิ่มความแม่นยำของเส้นขอบได้โดยไม่ต้องเปลี่ยน vtable ของอุปกรณ์เนื้อหาเดิม และไม่ทำลายความเข้ากันได้แบบไบนารีสำหรับโค้ดที่บิลด์ไว้กับเวอร์ชันก่อนหน้า กล่าวโดยเจาะจงคือ SVG ที่ส่งออกจะได้รับคีย์เวิร์ดที่แปลแล้วแทนที่จะเป็นจำนวนเต็มดิบของ PDF:

// ค่าตัวเลขจำนวนเต็มของ PDF ถูกแปลงเป็นแอตทริบิวต์คีย์เวิร์ดของ SVG
//   line cap  0, 1, 2  ->  butt, round, square
//   line join 0, 1, 2  ->  miter, round, bevel
//
// ความกว้างเส้นเป็นศูนย์หมายถึงเส้นบางระดับพื้นที่อุปกรณ์ใน PDF ดังนั้น
// ตัวส่งออกจึงใส่ vector-effect="non-scaling-stroke" เพื่อให้
// เส้นยังคงมองเห็นได้และมีความกว้างใกล้เคียงหนึ่งพิกเซลอุปกรณ์หลังผ่าน CTM
//
// f* B* b* จะเลือกกฎ even-odd และใส่ fill-rule="evenodd"
// ในขณะที่ f B b จะคงค่าเริ่มต้นของ SVG ที่เป็น nonzero winding

การเรียกคืนสถานะที่คำสั่ง Q ครอบคลุมความทึบแสง ความกว้างเส้น ปลายเส้น มุมต่อเส้น ค่าจำกัดมุมแหลม อาร์เรย์เส้นประ และเฟสเส้นประพร้อมกันทั้งหมด Form XObject ที่ซ้อนกันจะบันทึกและเรียกคืนชุดค่าครบชุดเดียวกันนี้ที่ขอบเขตของตัวเอง ดังนั้นเส้นขอบแบบประที่กำหนดไว้ภายในตราประทับจึงไม่สามารถรั่วไหลรูปแบบของมันเข้าไปในเนื้อหาหน้าที่ตามมาได้ หากคุณติดตามพฤติกรรมการครอบตัดและ CTM อยู่แล้วด้วยเหตุผลอื่น นี่คือแบบจำลองสถานะเดียวกับที่ปรากฏในการนำเข้าเวกเตอร์ EMF และ WMF เพียงแต่ทำงานในทิศทางตรงกันข้าม

ขอบเขตที่ควรรู้ก่อนนำไปใช้งานจริง

ตัวส่งออกซื่อตรงต่อขอบเขตความสามารถของตัวเอง และการรู้ขีดจำกัดล่วงหน้าย่อมมีต้นทุนถูกกว่าการมาพบเจอในระบบใช้งานจริง สีจะไปถึงอุปกรณ์ SVG ผ่านตัวดำเนินการ rg, RG, g และ G การเติมสีที่กำหนดผ่านพื้นที่สีบวกกับ scn ซึ่งเป็นวิธีที่ใช้วาดสี Separation, DeviceN และ ICCBased จะไม่มาถึงอุปกรณ์ในรูปแบบสาม RGB ที่คลี่คลายแล้ว ดังนั้นหน้าที่ใช้สีเฉพาะจุดด้วยวิธีนี้จึงส่งออกได้เฉพาะรูปทรงเรขาคณิตแต่ไม่ได้สีเหล่านั้น สำหรับต้นฉบับที่มุ่งเน้นงานพิมพ์ ให้แรสเตอร์แทนหรือ flatten สีเฉพาะจุดก่อน แบบจำลองการวาดสีเองอธิบายไว้ในการเรนเดอร์สีเฉพาะจุด Separation และ DeviceN

ข้อสังเกตเล็กๆ อีกสองข้อช่วยประหยัดเวลาแก้ไขบั๊ก ค่าคงที่สีเลขฐานสิบหกจะถูกส่งออกเป็นตัวพิมพ์ใหญ่ ดังนั้นการทดสอบที่ยืนยันค่า #ff0000 จะล้มเหลวเมื่อเทียบกับ #FF0000 ที่ถูกต้องสมบูรณ์แบบ และอุปกรณ์ SVG นับการอ้างอิงผ่านอินเทอร์เฟซของมัน ซึ่งหมายความว่าการปลดปล่อยมันคือการปล่อยให้อินเทอร์เฟซหลุดออกจากขอบเขตแทนที่จะเรียก Free บนอ็อบเจกต์นั้นโดยตรง ข้อแตกต่างนี้สำคัญหากคุณขยายอุปกรณ์เพื่อส่งออกมาร์กอัปของคุณเองควบคู่ไปกับเนื้อหาหน้า

การส่งออก SVG จับคู่ได้อย่างเป็นธรรมชาติกับการเปรียบเทียบเชิงโครงสร้างเมื่อคุณต้องการรู้ว่าเอกสารที่สร้างขึ้นเปลี่ยนแปลงไปจริงหรือไม่ระหว่างสองรุ่นบิลด์ ชุดเครื่องมือที่กว้างขึ้นรอบเอกสารที่โหลดไว้ ตั้งแต่การเรนเดอร์ไปจนถึงการแก้ไขและการส่งออก มีอธิบายไว้ในหน้าคอมโพเนนต์ PDF ของ HotPDF สำหรับ Delphi