Technical Article

การเพิ่มรูปภาพ JPEG 2000 ลงใน PDF ใน Delphi ด้วย HotPDF

ภาพสแกนสไลด์ทางการแพทย์ แผ่นภาพจากการสำรวจทางอากาศ เฟรมภาพยนตร์ที่ถูกเก็บถาวรด้วยช่วงไดนามิก (dynamic range) แบบเต็มรูปแบบ ภาพเหล่านี้คือภาพที่มาในรูปแบบ JPEG 2000 และที่มันมาในรูปแบบนั้นก็มีเหตุผล รูปแบบนี้จะเก็บความละเอียด 12 หรือ 16 บิตต่อแชนเนล บีบอัดด้วยการแปลงแบบเวฟเล็ต (wavelet transform) แทนที่จะใช้ block DCT แบบที่ JPEG ใช้ และสามารถเข้ารหัสรูปภาพเดียวกันได้ทั้งแบบไม่สูญเสียข้อมูล (lossless) หรือสูญเสียข้อมูล (lossy) จากโค้ดสตรีมเดียว เมื่อเอกสารที่สร้างขึ้นจากแหล่งข้อมูลเหล่านั้นจำเป็นต้องกลายเป็น PDF รูปภาพจะต้องเดินทางผ่านตัวกรองที่ข้อกำหนดเฉพาะของ PDF สงวนไว้สำหรับตัวแปลงสัญญาณนี้โดยเฉพาะ

HotPDF v2.228.0 ได้กู้คืนเอนจินถอดรหัส JPEG 2000 ที่ใช้งานได้สำหรับเส้นทางนั้น บิลด์ก่อนหน้านี้ได้ให้ยูนิตที่มาพร้อมกับฟังก์ชันจำลอง (stub functions) ที่ส่งคืนค่า nil ดังนั้นแม้ว่าจะมี API อยู่แต่มันก็ไม่ได้ถอดรหัสอะไรเลย เอนจินปัจจุบันจะผูกกับ OpenJPEG 2.5.4 แบบสแตติกและเปลี่ยนซอร์ส JP2 หรือ J2K ให้เป็นพิกเซลที่ HotPDF สามารถนำไปวางลงบนหน้าเอกสารได้

ตัวกรอง JPXDecode ใน PDF

ISO 32000-1 กำหนดตัวกรอง JPXDecode ไว้ใน §7.4.9 ออบเจ็กต์รูปภาพ PDF แบบ XObject จะระบุชื่อการบีบอัดในรายการ /Filter ของพจนานุกรมสตรีม และ JPXDecode ก็คือค่าที่บอกว่าข้อมูลสตรีมนั้นเป็นโค้ดสตรีมของ JPEG 2000 แทนที่จะเป็น JPEG พื้นฐานที่ /DCTDecode นำมา ตัวกรองนี้เป็นสิ่งที่ช่วยให้ PDF สามารถเก็บข้อมูลรูปภาพที่ถูกบีบอัดแบบเวฟเล็ตซึ่งมีความลึกของบิตสูงไว้ได้ และมันก็รองรับทั้งโหมดที่ไม่สูญเสียข้อมูลและโหมดที่สูญเสียข้อมูลของตัวแปลงสัญญาณ เนื่องจากโหมดนั้นเป็นคุณสมบัติของตัวโค้ดสตรีมเอง ไม่ใช่ของแรปเปอร์ที่ห่อหุ้มมันอยู่

ประเด็นสุดท้ายนั้นเป็นสิ่งที่ควรค่าแก่การจดจำ JPEG 2000 เป็นอัลกอริทึมเดียวที่มีกรณีพิเศษแบบไม่สูญเสียข้อมูล ไม่ใช่รูปแบบที่แยกกันสองรูปแบบ เวฟเล็ต 5/3 แบบผันกลับได้จะสร้างตัวอย่างดั้งเดิมขึ้นมาใหม่ได้อย่างแม่นยำ ในขณะที่เวฟเล็ต 9/7 แบบผันกลับไม่ได้จะแลกความแม่นยำนั้นกับขนาดไฟล์ที่เล็กลง ตัวถอดรหัสจะจัดการกับทั้งสองรูปแบบด้วยวิธีเดียวกันในเวลาที่อ่านข้อมูล ซึ่งนี่คือเหตุผลที่ HotPDF ต้องการเพียงเส้นทางถอดรหัสเดียวเพื่อรับข้อมูลใดๆ ก็ตามที่สตรีม JPXDecode ส่งมาให้มัน

สิ่งที่ตัวถอดรหัสทำกับพิกเซล

ในกรณีทั่วไป ออบเจ็กต์รูปภาพ PDF แบบ XObject จะคาดหวังข้อมูลที่ 8 บิตต่อแชนเนลใน DeviceGray หรือ DeviceRGB แต่โดยปกติแล้ว JPEG 2000 มักจะเกินขีดจำกัดนั้น และรูปแบบแชนเนลของมันก็มีความครอบคลุมมากกว่าแรสเตอร์ที่ถูกแพ็กมา ดังนั้นตัวถอดรหัสจึงมีงานที่ต้องทำสามอย่างก่อนที่ข้อมูลจะสามารถนำไปใช้เป็นรูปภาพปกติได้

อย่างแรก แชนเนลที่มีความลึกของบิตสูงจะถูกสุ่มตัวอย่างใหม่ (resample) ให้เป็น 8 บิต ตัวอย่าง 12 บิตหรือ 16 บิตจะถูกลดขนาดลงให้อยู่ในช่วง 0 ถึง 255 เพื่อให้ผลลัพธ์กลายเป็นแรสเตอร์ 8 บิตแบบธรรมดา แชนเนลที่มีเครื่องหมาย (signed) จะถูกเลื่อนให้อยู่ในช่วงที่ไม่มีเครื่องหมาย (unsigned) ก่อน รายละเอียดนี้มีความสำคัญเพราะตัวมันเองก็ถือเป็นการสูญเสียข้อมูล ภาพสแกนระดับสีเทา 16 บิตจะสูญเสียช่วงโทนสีที่ลึกทันทีที่มันกลายเป็นรูปภาพ PDF 8 บิต ซึ่งเป็นการแลกเปลี่ยนที่ถูกต้องสำหรับผลลัพธ์บนหน้าจอและการพิมพ์ แต่ไม่เหมาะสำหรับการนำกลับไปเก็บถาวรใหม่

อย่างที่สอง ปริภูมิสี YCbCr (ซึ่งตัวแปลงสัญญาณเรียกว่า SYCC) จะถูกแปลงเป็น RGB บ่อยครั้งที่ JPEG 2000 จัดเก็บสีในรูปแบบ luma-chroma เพื่อประสิทธิภาพในการบีบอัด ซึ่งเป็นแนวคิดเดียวกับที่ใช้ใน JPEG พื้นฐาน และตัวถอดรหัสก็จะประยุกต์ใช้การแปลงผกผันตามมาตรฐานเพื่อให้หน้าเอกสารได้รับค่า RGB ที่แท้จริง

อย่างที่สาม แชนเนลที่ถูกสุ่มตัวอย่างย่อย (subsampled) จะถูกอัปแซมเปิลด้วยการจำลองแบบใกล้เคียงที่สุด (nearest-neighbor) แชนเนลสี (chroma) มักจะถูกจัดเก็บไว้ที่ความละเอียดเพียงครึ่งเดียว ดังนั้นตัวถอดรหัสจึงอ่านข้อมูลแชนเนลแต่ละส่วนตามขนาดและปัจจัยการสุ่มตัวอย่างของมันเอง จากนั้นจึงจำลองตัวอย่างเพื่อดึงทุกแชนเนลให้มีขนาดเท่ากับรูปภาพเต็มก่อนที่จะทำการสอดแทรก (interleave) วิธีการจำลองแบบใกล้เคียงที่สุดช่วยให้ขั้นตอนนี้ใช้ทรัพยากรน้อยลง สีที่นำมาเติมนั้นเป็นความถี่ต่ำตั้งแต่แรกอยู่แล้ว ดังนั้นผลกระทบที่มองเห็นได้จึงมีน้อยมาก

กล่อง JP2 กับโค้ดสตรีม J2K ดิบ

ไฟล์ JPEG 2000 มาในสองรูปแบบ และ HotPDF จะตรวจจับว่ามันกำลังอ่านรูปแบบใดอยู่จากไบต์แรกๆ แทนที่จะดูจากนามสกุลไฟล์ ไฟล์ JP2 เป็นคอนเทนเนอร์แบบโครงสร้างกล่อง มันเริ่มต้นด้วยกล่องลายเซ็นสิบสองไบต์ 00 00 00 0C 6A 50 20 20 และห่อหุ้มโค้ดสตรีมเอาไว้ควบคู่ไปกับกล่องที่อธิบายถึงปริภูมิสี ความละเอียด และข้อมูลอภิพันธุ์ (metadata) ส่วนโค้ดสตรีม J2K ดิบนั้นจะไม่มีคอนเทนเนอร์ใดๆ เลย และเริ่มต้นด้วยเครื่องหมาย SOC FF 4F FF 51 ตัวถอดรหัสจะอ่านไบต์นำหน้าเหล่านั้น จดจำลายเซ็น และเลือกตัวแปลงสัญญาณ OpenJPEG ที่ตรงกันในแต่ละกรณี

มีการจัดการทั้งสองรูปแบบเนื่องจากทั้งสองแบบมีให้พบเห็นในการใช้งานจริง อุปกรณ์จับภาพและคลังข้อมูลถาวรที่ต้องการข้อมูลอภิพันธุ์ประกอบจะปล่อยข้อมูลออกมาเป็น JP2 ส่วนเครื่องมือที่ต้องการข้อมูลเพย์โหลดที่เล็กที่สุดเท่าที่จะเป็นไปได้จะปล่อยโค้ดสตรีมเปล่าๆ ออกมา ชนิดของรูปแบบจะถูกจำลองเป็นการแจงนับ TJpeg2000FileType ที่มีสมาชิกคือ jtInvalid, jtJP2, jtJ2K และ jtJPT สมาชิก JPT ตั้งชื่อตัวแปรย่อยของการสตรีม JPIP ตัวตรวจจับลายเซ็นไบต์จะแยกแยะรูปทรงสองแบบที่มันสามารถถอดรหัสได้ นั่นคือ JP2 และ J2K และจะรายงานสิ่งอื่นใดว่าเป็น jtInvalid เพื่อให้ข้อมูลนำเข้าที่ไม่รองรับเกิดข้อผิดพลาดอย่างชัดเจนแทนที่จะสร้างข้อมูลขยะขึ้นมา

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 or J2K, auto-detected
      if Decoder.GetImageData(Pixels) then
        // Pixels is 8-bit interleaved, ColorComponents channels wide,
        // row-major top to bottom: ready for a DeviceGray/DeviceRGB XObject.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

การไม่สูญเสียข้อมูลและการสูญเสียข้อมูลในฝั่งเข้ารหัส

ตัวถอดรหัสจะอ่านทั้งสองโหมดโดยไม่ต้องมีการบอกล่วงหน้าว่าเป็นโหมดใด ตัวเลือกนี้จะกลายเป็นเพียงพารามิเตอร์เมื่อคุณทำในทางกลับกันและสร้างไฟล์ JPEG 2000 ซึ่ง HotPDF ก็สามารถทำได้ผ่านคลาส TJpeg2000Bitmap ซึ่งเป็นคลาสย่อยของ TBitmap ที่โหลดและบันทึกข้อมูลแรสเตอร์เป็น JP2 มีคุณสมบัติสองประการที่ควบคุมผลลัพธ์ LosslessCompression เป็นค่าบูลีนที่เลือกเวฟเล็ตแบบผันกลับได้เมื่อมีค่าเป็น true ส่วน CompressionQuality คือ TJpeg2000QualityRange ซึ่งเป็นจำนวนเต็มตั้งแต่ 1 ถึง 100 โดยที่ 1 จะมีขนาดเล็กและคุณภาพแย่ ส่วน 100 จะมีขนาดใหญ่และรักษาความเที่ยงตรง ค่าเริ่มต้นนั้นจะอยู่ในรูปแบบของค่าคงที่ที่มีชื่อ Jpeg2000DefaultLosslessCompression จะเป็น False และ Jpeg2000DefaultLossyQuality จะเป็น 80

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

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // decode an existing JP2/J2K
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // or, for a smaller lossy file:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // matches the default
    Bmp.SaveToStream(Output);                // always writes a JP2 file
  finally
    Bmp.Free;
  end;
end;

เหตุใดจึงไม่มีไปป์ไลน์ตัวกรองแบบถอดรหัสเมื่อโหลด

ข้อเท็จจริงทางสถาปัตยกรรมประการหนึ่งกำหนดวิธีที่คุณจะใช้งานสิ่งเหล่านี้ และมันก็เป็นเรื่องง่ายที่จะทึกทักเอาในทางตรงกันข้าม HotPDF ไม่มีตัวกรองรูปภาพทั่วไปสำหรับการถอดรหัสเมื่อโหลด เมื่อคุณเปิด PDF ที่มีรูปภาพแบบ JPXDecode อยู่แล้ว เอนจินจะไม่ถอดรหัสสตรีมนั้น มันจะเก็บไบต์ของ JPEG 2000 ไว้เหมือนเดิมทุกประการ ดังนั้นการคัดลอกหน้าหรือการผสานเอกสารจะนำพารูปภาพผ่านไปโดยไม่มีการถูกแก้ไขใดๆ แบบไบต์ต่อไบต์ ตัวถอดรหัสมีจุดเข้าใช้งานเพียงจุดเดียว และมันอยู่ฝั่งการสร้าง นั่นคือ AddImage ที่ทำงานบนพื้นฐานของไฟล์ ซึ่งจะกระจายตามนามสกุลไฟล์เพื่อจัดการกับซอร์สที่เป็น .jp2, .j2k, .jpt และ .jpc

การแยกส่วนดังกล่าวนั้นถือเป็นการออกแบบที่ถูกต้องมากกว่าที่จะเป็นข้อจำกัด การถอดรหัสสตรีม JPX ที่ฝังไว้ตอนที่โหลดเพียงเพื่อจะนำไปเข้ารหัสใหม่ตอนที่บันทึก จะเป็นการแปลงรูปภาพที่เก็บถาวรแบบไม่สูญเสียข้อมูลให้กลายเป็นแบบที่สูญเสียข้อมูล และจะทำให้ขนาดใหญ่ขึ้นในทุกการผสานเอกสาร ทั้งหมดนี้เกิดขึ้นกับรูปภาพที่คุณแค่ตั้งใจจะย้ายจาก PDF หนึ่งไปยังอีก PDF หนึ่งเท่านั้น การส่งผ่านสตรีมตามข้อความทุกตัวอักษรนั้นคือการทำงานแบบไม่สูญเสียข้อมูลและยังรวดเร็วอีกด้วย การถอดรหัสจะถูกเลื่อนออกไปจนกว่าจะถึงช่วงเวลาเดียวที่จำเป็นอย่างแท้จริงเท่านั้น นั่นคือเมื่อคุณส่งไฟล์ JPEG 2000 จากดิสก์ให้กับเอนจินและขอให้มันแปลงภาพนั้นเป็นแรสเตอร์เพื่อไปวางบนหน้าใหม่ ในจุดนั้นไฟล์จะต้องกลายเป็นพิกเซล และตัวถอดรหัสก็เริ่มทำงาน

การลงทะเบียนการสนับสนุนและการจัดวางรูปภาพ

การลงทะเบียนรูปภาพ JPEG 2000 เป็นตัวเลือกเบื้องหลังสวิตช์คอมไพล์ HPDF_REGISTER_JPEG2000_PICTURE ซึ่งปิดอยู่ตามค่าเริ่มต้น เหตุผลก็คือมันมีความขัดแย้งที่แท้จริง ไม่ใช่แค่ความระมัดระวัง การลงทะเบียนรูปแบบไฟล์ jp2, j2k และ jpc เป็นแบบโกลบอลด้วย TPicture อาจไปรบกวนการตรวจจับรูปแบบ BLOB ที่ TppDBImage ของ ReportBuilder ใช้เป็นหลัก ให้กำหนดสวิตช์นี้เมื่อการทำงานร่วมกันนั้นไม่ได้ถูกนำมาใช้ และรูปแบบไฟล์จะลงทะเบียนเพื่อให้ TPicture รู้จักมัน หากปล่อยไว้โดยไม่ได้กำหนด การกระจายนามสกุลของ AddImage จะยังคงถอดรหัสไฟล์ JPEG 2000 ได้โดยตรง เพราะเส้นทางนั้นไม่ได้ผ่าน TPicture เลย

เมื่อเข้าใจตรงกันแล้ว การจัดวางรูปภาพ JPEG 2000 ก็เป็นจังหวะการเรียกใช้งานสามขั้นตอนแบบเดียวกับรูปภาพ HotPDF อื่นๆ ส่งมอบเส้นทาง .jp2 และชนิดของการบีบอัดให้กับ AddImage สำหรับวิธีกำหนดว่ารูปภาพควรถูกเก็บไว้ในผลลัพธ์อย่างไร จากนั้นจึงจัดตำแหน่งดัชนีรูปภาพที่ส่งคืนมาไว้บนหน้ากระดาษด้วย ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // The .jp2 source is decoded through the OpenJPEG backend, then
    // re-embedded with the compression you request here.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, width, height in points; final 0 is the rotation angle.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

การบีบอัดที่คุณส่งไปยัง AddImage จะเป็นตัวควบคุมวิธีที่รูปภาพที่ถูกถอดรหัสมาจะถูกจัดเก็บอีกครั้ง ไม่ใช่วิธีที่มันถูกอ่านมา ไฟล์ JPEG 2000 ที่ถูกถอดรหัสมาเป็นบิตแมปสามารถส่งกลับออกไปเป็น DCTDecode JPEG, แรสเตอร์แบบ Flate หรือตัวกรองอื่นๆ ที่รองรับได้ แล้วแต่ว่าสิ่งใดจะเหมาะสมกับเอกสารนั้น การถอดรหัสจาก JP2 หรือ J2K จะเกิดขึ้นก่อนเสมอ ดังนั้นการเรียกใช้งานเดียวกันก็จะยอมรับแหล่งข้อมูลที่ถูกบีบอัดแบบเวฟเล็ตและนำไปฝังไว้ในรูปแบบใดก็ตามที่ขั้นตอนการทำงานที่เหลือของคุณคาดหวัง

สำหรับภาพรวมที่กว้างขึ้นเกี่ยวกับวิธีที่รูปภาพและแบบอักษรจะปรากฏในผลลัพธ์ที่สร้างขึ้น โปรดดู บันทึกของเราเกี่ยวกับผลลัพธ์รายงานที่มีแบบอักษรและรูปภาพ เมื่อเอกสารที่คุณกำลังรวบรวมมีการใช้เนื้อหาจาก PDF ที่มีอยู่ซ้ำ พฤติกรรมการส่งผ่านที่อธิบายไว้ที่นี่ก็จะจับคู่กับกลไกการผสานและการแก้ไขใน ออบเจ็กต์สตรีมและการอัปเดตแบบเพิ่มส่วน เอนจินการถอดรหัส JPEG 2000 มาพร้อมกับ HotPDF Component สำหรับ Delphi และ C++Builder ควบคู่ไปกับรูปภาพ แบบอักษร และ API ของเอกสารซึ่งได้อธิบายไว้ที่อื่นในบล็อกนี้