Artikel Teknis

Meratakan Anotasi PDF tanpa Stream /AP di Delphi

HotPDF v2.743.0 meratakan PDF annotation yang tidak memiliki appearance stream /AP, bukan melewatinya secara diam-diam. FlattenLoadedAnnotations kini mengarahkan widget tanpa appearance melalui EnsureLoadedFieldAppearanceStream dan membuat Form XObject untuk markup tanpa appearance dari property annotation-nya sendiri, sehingga nilai yang diketik ke form /NeedAppearances tetap masuk ke page content, bukan hilang saat flatten. Kegagalan yang memaksa perubahan ini terlihat seperti no-op. Customer mengirim application form yang diisi lalu dicetak ke PDF dari browser. Anda memuatnya di HotPDF, memanggil FlattenLoadedAnnotations, mendapatkan kembali 0, menyimpan, lalu mengirim dokumen dengan kotak kosong di tempat pemohon mengetik nama dan nominal. Tidak ada exception dan tidak ada log. Nilai tersebut sebenarnya berada di file sejak awal, tersimpan di entry /V setiap field, tetapi flatten pass melewatinya karena tidak satu pun widget membawa appearance stream untuk dibake

Mengapa flattening form yang dicetak browser menghilangkan nilai yang diketik?

Karena form /NeedAppearances menyimpan nilai tanpa menyimpan gambar nilainya. ISO 32000-1 12.7.2 mengizinkan interactive form menetapkan /NeedAppearances true di AcroForm dictionary, yang memberi tahu viewer untuk membangun visual surface setiap field saat open dari /V, /DA, dan /Q. Producer yang membuat form secara murah — jalur print browser, filler server-side, beberapa scanning front end — memanfaatkan fasilitas itu dan tidak menulis /AP sama sekali. Flattening, sebagaimana didefinisikan oleh appearance algorithm dalam ISO 32000-1 12.5.5, adalah pekerjaan transcription: ambil normal appearance stream annotation, petakan /BBox-nya ke /Rect, panggil dari page content stream dengan operator Do, lalu hapus annotation. Tanpa source stream tidak ada yang bisa ditranskripsikan. Implementasi HotPDF lama, sejak v2.386.0, memperlakukan kondisi itu sebagai "skip", yang masuk akal secara terpisah tetapi buruk secara keseluruhan: dokumen yang paling membutuhkan flattening justru paling kecil kemungkinannya membawa appearance. Celah yang sama juga menelan markup — Highlight dari review tool, Square dari redline pass, Ink signature — setiap kali producer mengandalkan viewer untuk menggambarnya

Di mana HotPDF memasukkan synthesis ke dalam FlattenLoadedAnnotations?

Titik hook sengaja diletakkan terlambat: setelah lookup appearance gagal, bukan sebelumnya. FlattenLoadedAnnotations tetap lebih dulu meminta normal appearance melalui GetLoadedAnnotationAppearanceStream, dan annotation yang sudah memiliki appearance tetap dibake persis seperti pada v2.386.0. Hanya hasil nil, pada annotation dengan /Rect non-degenerate dan tanpa hidden flag, yang masuk ke synthesis path. Urutan ini penting: author dokumen yang bersusah payah menulis /AP mendapatkan kembali byte miliknya, bukan rekonstruksi HotPDF atas byte tersebut

NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
  if Subtype= 'Widget' then
  begin
    FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
    if FieldIdx>= 0 then
      EnsureLoadedFieldAppearanceStream(FieldIdx);
    // tanya lagi: generator telah menambahkan /AP /N ke widget
    NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
  end
  else
    NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;

Dari sini kedua keluarga annotation bercabang. Widget di-resolve kembali ke field pemiliknya melalui GetLoadedFormFieldIndexForAnnotation lalu diserahkan ke EnsureLoadedFieldAppearanceStream, yaitu field appearance generator yang sudah ada di Delphi PDF library ini sejak v2.328.0. Menggunakannya kembali, alih-alih menulis field renderer kedua, adalah inti desainnya — generator itu sudah mencakup Type0 font, line wrapping, quadding, state /AS checkbox dan radio, serta rotasi /MK, machinery yang sama di balik penambahan AcroForm field ke PDF yang sudah dimuat. Semua yang lain masuk ke markup synthesizer. Bagi caller tidak ada perubahan: flatten call satu baris yang sama kini mengembalikan count non-zero pada dokumen yang sebelumnya mengembalikan nol

Doc:= THotPDF.Create(nil);
try
  Doc.LoadFromFile('needappearances-form.pdf');
  // v2.743.0: widget dan markup tanpa AP disintesis, lalu dibake
  Flattened:= Doc.FlattenLoadedAnnotations;          // semua halaman, semua subtype
  // Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
  if Flattened= 0 then
    raise Exception.Create('nothing was flattened');
  Doc.SaveLoadedDocument('flattened.pdf');
finally
  Doc.Free;
end;

Mengapa QuadPoints dan InkList jatuh di tempat yang salah?

Karena koordinat tersebut berada di page user space, sedangkan synthesized appearance stream menggambar di ruang /BBox-nya sendiri, dan kedua origin itu bukan titik yang sama. ISO 32000-1 Table 176 mendefinisikan /QuadPoints untuk text markup annotation dalam default user space, dan Table 174 melakukan hal yang sama untuk endpoint /L pada line annotation; /InkList mengikuti konvensi yang sama. HotPDF memberi synthesized form sebuah /BBox berupa [0 0 W H] dengan origin di lower-left corner /Rect. Jadi setiap point yang diambil dari /QuadPoints, /L, atau /InkList harus ditranslasikan dengan negasi lower-left /Rect sebelum ditulis ke content stream. Salah di sini dan highlight pada line yang berada 700 point di atas halaman akan digambar 700 point di atas kotaknya sendiri, yang dalam praktik berarti tidak terlihat. Perbaikannya hanya satu pengurangan per koordinat, dan perbaikan itu menyusun dirinya dengan cm yang diterbitkan bake setelahnya — matrix tersebut memetakan /BBox kembali ke /Rect, sehingga kedua langkah membatalkan satu sama lain menuju geometri absolut yang benar

// Endpoint /L berada di page user space (ISO 32000-1 Table 174); origin
// BBox form berada di lower-left /Rect, jadi geser dengan -(RL, RB)
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
  StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
  _FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
  _FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;

Apa yang sebenarnya digambar oleh synthesized markup appearance?

Markup synthesizer membaca annotation dictionary dan tidak membaca hal lain, sehingga output tetap dapat diprediksi dan tetap jujur tentang hal yang tidak dapat diketahuinya. FreeText dan Stamp menggambar /Contents menggunakan font dan warna yang diparse dari /DA, di-align berdasarkan /Q, dengan padding 2 pt. Square dan Circle menggambar outline re atau Bezier empat-arc yang di-stroke dalam /C, di-fill dengan /IC jika ada, pada width dari /BS /W. Line dan Ink men-stroke vertex-nya. Highlight mengisi setiap quad, sedangkan Underline, StrikeOut, dan Squiggly men-stroke garis di bagian bawah quad, titik tengah quad, atau zigzag satu point. /CA di bawah 1 menjadi ExtGState dengan entry ca, yang direferensikan sebagai /GSA gs di bagian awal stream

Encoding teks ditentukan dari entry AcroForm /DR /Font yang disebut oleh /DA. Jika /Subtype font tersebut adalah Type0, HotPDF menulis string sebagai UTF-16BE hex literal dengan byte order mark FEFF; jika tidak, HotPDF menulis escaped literal string, dengan tanda kurung dan backslash di-escape serta byte di atas 126 ditulis dalam octal. Operator Tf dari /DA diterbitkan sebelum BT, yang legal karena text state bertahan melintasi boundary text object, dan cara ini menghindari pemecahan string /DA. Dua batasan perlu dinyatakan dengan jelas. Line width untuk wrapping dan quadding diestimasi dengan heuristic half-em / full-em, bukan font metric sebenarnya, sehingga alignment pada proportional font mendekati tetapi tidak persis. Selain itu, subtype tanpa sesuatu yang dapat disintesis — Popup, Link, atau Stamp yang satu-satunya content adalah nama icon — menghasilkan nil dan dibiarkan tanpa perubahan, persis seperti sebelumnya

Temporary /Annots swap yang dihukum oleh cleanup yang terlalu membantu

FlattenOneWidget, path per-widget yang digunakan oleh FlattenLoadedFormFields, adalah aliasing trap yang harus dihormati oleh setiap perubahan di dalam shared flatten loop. Method tersebut sementara mengganti value /Annots halaman dengan array satu elemen agar generic flatten pass beroperasi pada satu widget, lalu mengembalikan pointer PHPDFDictionaryItem asli dalam blok finally. Restore tersebut menulis kembali ke dictionary slot yang disimpan sebelum pemanggilan

DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
  Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
  DictItem^.Value:= Item;   // dangling jika inner loop membebaskan item ini
  TemporaryAnnots.Free;
end;

Tambahkan cleanup yang tampak masuk akal di dalam shared inner loop — DeleteValue('Annots') setelah array kosong, agar page yang disimpan tidak membawa empty array yang tersisa — dan pemanggilan itu membebaskan dictionary item yang ditunjuk DictItem. finally kemudian menulis melalui dangling pointer dan proses mati dengan "Invalid pointer operation". Dua test yang sudah ada langsung menangkapnya, dan itu satu-satunya alasan masalah ini hanya menjadi catatan kaki, bukan support ticket. Aturannya berlaku umum: sebelum menambahkan cleanup ke shared loop, periksa caller untuk kontrak alias atau swap. Empty /Annots array yang tersisa hanya cacat kosmetik, tidak sepadan dengan menukar jaminan lifetime pointer

Apa yang tetap tidak dibake, dan berapa biaya flattening?

Hidden annotation sengaja dikecualikan. Annotation dengan integer /F yang bit position 2-nya diset bersifat hidden menurut ISO 32000-1 12.5.3, dan ketika juga tidak memiliki /AP ada godaan untuk mensintesis lalu membakarnya seperti yang lain. Itu akan menjadi bug dengan konsekuensi keamanan: membake note yang tidak terlihat ke page content membuatnya terlihat bagi setiap orang yang membuka file. HotPDF membiarkan annotation tersebut persis di tempatnya dan tidak menghitungnya dalam return value. Jelaskan pula dengan tegas kepada user tentang harga annotation yang benar-benar dibake. Flattening bersifat irreversible — annotation dihapus dari array /Annots halaman dan visualnya kini menjadi page content, sehingga tidak ada lagi editing field value, comment thread, toggle state /AS, atau cara memulihkan structured data selain dari file asli. Flatten salinan, simpan original, dan gunakan hanya ketika dokumen berhenti menjadi form lalu menjadi record. Jika masalah Anda didukung XFA, bukan appearance-less, jalur terpisah XFA ke AcroForm flattening di HotPDF adalah titik awalnya, dan jika Anda masih membangun form, catatan tentang menghubungkan AcroForm field action dan validation membahas sisi write

Ada satu caveat verifikasi karena jika tidak, Anda bisa kehilangan satu sore. ExtractLoadedPageGlyphs tidak menelusuri Form XObject, sedangkan baked appearance berada di dalam satu Form XObject — page content stream hanya memuat urutan q ... cm /FlatAn<n> Do Q. Karena itu glyph extraction pada halaman yang sudah di-flatten tidak mengembalikan apa pun, dan perilaku tersebut benar, bukan bake yang hilang. Verifikasi pada byte level dengan memeriksa resource name /FlatAn, pemanggilan Do, dan /Subtype /Form, atau melalui rendering pipeline yang memang melakukan expand XObject

Annotation flattening tampak seperti tiga baris transcription sampai Anda bertemu dokumen yang benar-benar dibuat orang. Jika Anda bekerja dengan filled form, review markup, atau output archival di Delphi atau C++Builder, ada baiknya membaca bagaimana HotPDF Delphi PDF component menangani sisi loaded-document AcroForm dan annotation sebelum membangun appearance generator sendiri di atasnya