Anotasi
Tambahkan dukungan anotasi ke viewer
Anotasi di Doconut berfungsi dalam dua arah: pengguna menggambarnya di widget peramban dan server menyimpannya per halaman, atau kode Anda membuatnya dan memuatnya ke dalam sesi terbuka. Kedua cara tersebut menampilkan anotasi pada halaman dan dapat dibakar ke dalam ekspor PDF atau PNG.
Dukungan anotasi dibatasi oleh kemampuan lisensi Annotation (diberikan secara otomatis di bawah lisensi Temporary yang aktif).
Aktifkan UI anotasi
Anotasi adalah modul Viewer, bukan toolbar mandiri. Halaman harus menyertakan sumber daya Viewer, toolbar Viewer, mount Viewer, dan objViewer yang telah diinisialisasi; Ribbon Anotasi kemudian dipasang dan dilampirkan ke instance yang sama.
Keluarkan file anotasi bersama file viewer. Mereka dibatasi oleh lisensi, sehingga tag mereka muncul hanya ketika kemampuan diberikan. Pada MVC 5, bangun tag di dalam controller (view tidak memiliki @inject):
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
ViewBag.ViewerCss = viewer.ReferenceCss(new CssConfig
{
IncludeViewerCss = true,
IncludeAnnotationCss = true // jquery-ui.min.css + annotationBar.css
});
ViewBag.ViewerScripts = viewer.ReferenceScripts(new ScriptConfig
{
IncludeJQuery = true,
IncludeViewerScripts = true,
IncludeAnnotationScripts = true, // jquery-ui, raphael.js, annotation.js
IncludeAnnotationBar = true // pita anotasi yang disematkan
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;Pertahankan komposisi Viewer lengkap dalam markup:
<nav id="toolbar" aria-label="Document viewer controls">
<!-- Kontrol Viewer, termasuk tombol yang membuka Anotasi -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>File Anotasi menghasilkan Ribbon di dalam annBarMount; Anda tidak menyalin tombol atau dialognya. Inisialisasi docViewer terlebih dahulu, kemudian buat Ribbon hanya ketika server mengonfirmasi bahwa Anotasi berlisensi:
@Html.Raw(ViewBag.ViewerScripts)
<script>
var annBar = null;
var currentToken = '';
var objViewer = $('#div_ctlDoc').docViewer({
BasePath: '/',
ResPath: 'doconut-res/images',
onAnnLoaded: function () { if (annBar) annBar.handleAnnLoaded(); },
onAnnSaved: function () { if (annBar) annBar.handleAnnSaved(); },
onAnnSaveError: function () { if (annBar) annBar.handleAnnSaveError(); },
onAnnClosed: function () { if (annBar) annBar.handleAnnClosed(); },
onError: function (message) { console.error('Viewer error:', message); }
});
@if ((bool)ViewBag.IsAnnotationEnabled)
{
<text>
annBar = $('#annBarMount').doconutAnnotationBar({
docId: 'ctlDoc',
getRequestParams: function () { return { token: currentToken }; },
onLayout: function () { requestAnimationFrame(function () { objViewer.Refit(); }); }
});
annBar.attach(objViewer);
</text>
}
</script>Tanpa kemampuan tersebut, ReferenceScripts tidak menyertakan skrip Ribbon, sehingga pernyataan if di sisi server mencegah pemanggilan doconutAnnotationBar(...) yang tidak terlindungi merusak halaman. Pada Web Forms, tulis penjagaan yang sama sebagai <% if (IsAnnotationEnabled) { %> … <% } %>.
Menyimpan dari Ribbon mengirim data ke DocImage.axd (AnnSave), yang menyimpannya dalam sesi dokumen per halaman. Memuat (AnnLoad) terjadi secara otomatis ketika halaman dengan anotasi dirender.
Buka dan tutup Ribbon dari toolbar Anda sendiri dengan annBar.open() dan annBar.close(). API publiknya:
| Metode | Tujuan |
|---|---|
attach(objViewer) | Hubungkan Ribbon ke viewer yang telah diinisialisasi; diperlukan satu kali |
open() / close() | Masuk atau keluar dari penyuntingan anotasi |
reset() | Kembalikan Ribbon ke keadaan tertutup, tidak dalam penyuntingan |
isOpen() / annotating() | Baca status Ribbon / status penyuntingan anotasi pada viewer |
reopenEditable() | Muat ulang anotasi halaman saat ini sebagai objek yang dapat disunting |
updateActionState() | Segarkan kontrol simpan dan hapus setelah perubahan host |
headerSlot() | Slot header opsional untuk kontrol milik host |
onStatus, onToast, onLayout, onEditStart, dan onEditEnd adalah callback host opsional. Objek endpoints juga dapat menyediakan exportPdf, exportPng, imageUpload, dan imageList; kontrol tanpa endpoint yang dikonfigurasi tetap tersembunyi.
Dua jenis callback, dihubungkan dua cara
Kode anotasi menggunakan dua gaya callback, dan masing‑masing gagal secara diam ketika dihubungkan dengan cara yang lain:
onAnnLoaded,onAnnSaved,onAnnSaveError,onAnnClosedadalah opsi daridocViewer({...}), seperti setiap callback viewer.Created,Changed,Deleted, danPropertiesadalah fungsi global yang ditemukan berdasarkan nama: id viewer diprefikskan ke acara. Untuk viewer di#div_ctlDocid‑nya adalahctlDoc, sehingga fungsi‑fungsinya adalahctlDoc_Created,ctlDoc_Changed,ctlDoc_Deleted, danctlDoc_Properties. Fungsi yang salah eja atau diubah namanya tidak akan pernah dipanggil.
SaveAnnotations() tidak mengembalikan apakah penyimpanan berhasil: hasilnya datang melalui onAnnSaved atau onAnnSaveError. Kode yang membaca nilai kembaliannya tidak pernah membersihkan flag “perubahan belum disimpan”.
Membuat anotasi dalam C#
Dapatkan manager yang terikat pada sesi terbuka, tambahkan anotasi, dan muat mereka (dengan using Doconut.Annotations; untuk tipe‑tipe dan using System.Drawing; untuk Rectangle dan Color):
// POST /Annotations/LoadSample?token=...
[HttpPost]
public ActionResult LoadSample(string token)
{
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
// Terikat pada dimensi halaman sesi terbuka
var manager = viewer.GetAnnotationManager(token);
var pageCount = viewer.GetPageCount(token);
// Satu stempel per halaman
for (int page = 1; page <= pageCount; page++)
{
manager.Add(new StampAnnotation(page, new Rectangle(30, 20, 240, 90),
$"PAGE {page}", 28, 4, Color.Maroon)
{
Opacity = 60,
Rotate = -8
});
}
manager.Add(new NoteAnnotation(1, new Rectangle(420, 150, 220, 120),
"Loaded from C# code.", Color.FromArgb(255, 255, 255, 170), 14));
// Muat ke dalam sesi - viewer mengambilnya, dan renderer menempelkannya ke dalam
// ekspor gambar/PDF.
viewer.LoadAnnotationData(token, manager);
return new HttpStatusCodeResult(200);
}Jenis Anotasi
Semua tipe berada di Doconut.Annotations dan mewarisi dari BaseAnnotation (nomor halaman plus Rectangle pembatas):
| Tipe | Catatan |
|---|---|
StampAnnotation | Stempel teks dengan ukuran font, border, warna; mendukung Opacity, Rotate |
NoteAnnotation | Catatan tempel dengan teks, warna latar belakang, ukuran font, TitleColor |
RectangleAnnotation | Warna border dan isi, Title/ShowTitle |
CircleAnnotation | Border dan isi, ShowBorder |
EllipseAnnotation | Border dan isi, ShowBorder |
TriangleAnnotation | Warna border, BackColor, ShowBorder |
LineAnnotation | Garis lurus dengan lebar dan warna |
ArrowAnnotation | Garis dengan kepala panah; dapat diatur Direction (ArrowDirection, titik kompas, default E) |
FreehandAnnotation | Garis bebas dari titik FreehandData yang terenkode |
ImageAnnotation | Gambar dari URL. URL relatif diresolusikan terhadap host permintaan saat ini ketika anotasi ditambahkan — lihat catatan ekspor di bawah |
API AnnotationManager
| Anggota | Tujuan |
|---|---|
Add(BaseAnnotation) | Antrian anotasi |
GetAnnotations() / GetAnnotations(int page) | Periksa apa yang dimiliki manager |
ClearAnnotations() / ClearAnnotations(int page) | Hapus semua / per halaman |
GetAnnotationData() / GetAnnotationData(int page) | Data anotasi terenkode — amplop yang dikonsumsi widget |
GetAnnotationXml() | Bentuk XML |
Viewer mencerminkan operasi muat dan baca terhadap sebuah sesi: LoadAnnotationData(token, manager) atau LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), dan GetAnnotationXML(token).
Ekspor dengan anotasi dibakar di dalam
// PDF semua halaman dengan anotasi yang dirender di atasnya
public async Task<ActionResult> ExportPdf(string token)
{
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
byte[] pdf = await viewer.ExportAnnotationsToPdfAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
return File(pdf, "application/pdf", "export.pdf");
}
// Atau ZIP dari PNG per halaman
public async Task<ActionResult> ExportPngZip(string token)
{
var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();
byte[] zip = await viewer.ExportAnnotationsToPngZipAsync(token, zoom: 100, ct: Response.ClientDisconnectedToken);
return File(zip, "application/zip", "annotations-png.zip");
}Ekspor menggunakan pembakar yang sama seperti rendering di layar, serta lisensi dan keputusan watermark khusus yang sama.
Anotasi gambar pada host intranet diekspor sebagai "Image blocked"
Penampil dan proses ekspor mengambil gambar ImageAnnotation dari tempat yang berbeda:
| Permukaan | Siapa yang mengunduh gambar | Pada host intranet atau pengembangan |
|---|---|---|
| Penampil di peramban | Peramban pengguna | Gambar muncul |
ExportAnnotationsToPdfAsync / …PngZipAsync | Server | Penampung berwarna merah "Image blocked" |
Ketika server mengambil gambar untuk disematkan, ia menolak alamat yang mengarah kembali ke jaringan pribadi: loopback (localhost, 127.0.0.1), 0.0.0.0, link‑local (169.254.x.x), rentang pribadi 10.x, 172.16–31.x, dan 192.168.x, carrier‑grade NAT (100.64.0.0/10), serta alamat link‑local dan unique‑local IPv6. Hal ini melindungi server dari penyalahgunaan untuk mengakses sistem internal melalui URL gambar yang dibuat khusus, dan tidak ada pengaturan untuk mematikannya.
URL gambar relatif diresolusikan terhadap host dari permintaan saat ini, sehingga pada localhost, pada server intranet, atau pada situs mana pun yang diakses melalui alamat pribadi, gambar akan muncul di penampil dan ekspor akan berisi penampung sebagai gantinya. Itulah mekanisme perlindungan yang bekerja, bukan kegagalan rendering. Jika ekspor harus menyertakan gambar, layani gambar dari host dengan alamat publik dan gunakan URL absolut. URL gambar yang tidak dapat diresolusikan sama sekali akan membuat anotasi tersebut menghilang tanpa error — periksa piksel yang dirender atau log jaringan, bukan kode status.
Alur Kerja Persistensi
- Buka dokumen dan dapatkan token‑nya.
- Muat XML atau data terenkode yang sebelumnya disimpan ke dalam token tersebut.
- Biarkan widget membaca dan mengedit anotasi sesi.
- Ambil XML dengan
GetAnnotationXML(token)ketika aplikasi Anda memutuskan untuk menyimpan. - Ekspor PDF atau PNG ketika diperlukan hasil akhir yang diratakan.
- Tutup sesi dokumen.
Jangan gunakan token penampil sebagai pengidentifikasi anotasi permanen — token akan hilang bersama sesi, dan daur ulang application pool akan mengakhiri setiap sesi pada satu server. Simpan data anotasi dengan mengaitkannya pada dokumen dan pengidentifikasi versi Anda sendiri.
Catatan Keamanan dan Rendering
- Permintaan anotasi menggunakan keamanan sesi dan token yang sama seperti permintaan halaman.
- Validasi dan kontrol setiap URL gambar yang diberikan pengguna.
- Payload bebas tangan yang besar dan ekspor resolusi tinggi meningkatkan penggunaan memori; uji dokumen realistis dan nilai zoom.
Pemecahan Masalah
| Gejala | Pemeriksaan |
|---|---|
| Ribbon Anotasi tidak muncul | Kemampuan Annotation dan empat flag CSS/script anotasi |
| Penyimpanan melaporkan error | Token atau kedaluwarsa sesi, dan BasePath widget |
ctlDoc_Created (atau fungsi event lain) tidak pernah dijalankan | Nama fungsi: id penampil + _ + event, sebagai fungsi global |
| Anotasi C# tidak muncul | Nomor halaman mulai dari satu, dan data dimuat ke dalam token aktif |
| Anotasi gambar muncul di layar tetapi tidak di ekspor | Host gambar memiliki alamat privat atau loopback — lihat di atas |
| Dokumen yang dibuka kembali tidak memiliki anotasi | Persistensikan XML atau data di luar sesi penampil, lalu muat ke dalam token baru |
Apakah halaman ini membantu?