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

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

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

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

สำหรับงานทั่วไปอย่างการส่งออกหน้าเพื่อใช้กับตัวแสดงผลบนเว็บ เครื่องมือเทียบความต่าง หรือการส่งมอบงานออกแบบ พื้นผิว 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 ประกอบ pre-transform การหมุนหน้าสำหรับ /Rotate 90, 180 และ 270 และคูณ CTM ด้านขวาด้วย matrix(1,0,0,-1,0,1) การส่งออก PDF เป็น SVG จาก Delphi จึงคงรูปภาพที่ฝังไว้ให้ตั้งตรง
การ pre-transform การหมุนหน้าและเมทริกซ์พลิกรูปภาพ ทำให้หน้าที่หมุนแล้วกับภาพถ่ายยังตั้งตรงใน SVG ที่ส่งออก

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

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