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):

csharp
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:

html
<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
@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:

MetodeTujuan
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, onAnnClosed adalah opsi dari docViewer({...}), seperti setiap callback viewer.
  • Created, Changed, Deleted, dan Properties adalah fungsi global yang ditemukan berdasarkan nama: id viewer diprefikskan ke acara. Untuk viewer di #div_ctlDoc id‑nya adalah ctlDoc, sehingga fungsi‑fungsinya adalah ctlDoc_Created, ctlDoc_Changed, ctlDoc_Deleted, dan ctlDoc_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):

csharp
// 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):

TipeCatatan
StampAnnotationStempel teks dengan ukuran font, border, warna; mendukung Opacity, Rotate
NoteAnnotationCatatan tempel dengan teks, warna latar belakang, ukuran font, TitleColor
RectangleAnnotationWarna border dan isi, Title/ShowTitle
CircleAnnotationBorder dan isi, ShowBorder
EllipseAnnotationBorder dan isi, ShowBorder
TriangleAnnotationWarna border, BackColor, ShowBorder
LineAnnotationGaris lurus dengan lebar dan warna
ArrowAnnotationGaris dengan kepala panah; dapat diatur Direction (ArrowDirection, titik kompas, default E)
FreehandAnnotationGaris bebas dari titik FreehandData yang terenkode
ImageAnnotationGambar dari URL. URL relatif diresolusikan terhadap host permintaan saat ini ketika anotasi ditambahkan — lihat catatan ekspor di bawah

API AnnotationManager

AnggotaTujuan
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

csharp
// 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:

PermukaanSiapa yang mengunduh gambarPada host intranet atau pengembangan
Penampil di perambanPeramban penggunaGambar muncul
ExportAnnotationsToPdfAsync / …PngZipAsyncServerPenampung 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

  1. Buka dokumen dan dapatkan token‑nya.
  2. Muat XML atau data terenkode yang sebelumnya disimpan ke dalam token tersebut.
  3. Biarkan widget membaca dan mengedit anotasi sesi.
  4. Ambil XML dengan GetAnnotationXML(token) ketika aplikasi Anda memutuskan untuk menyimpan.
  5. Ekspor PDF atau PNG ketika diperlukan hasil akhir yang diratakan.
  6. 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

GejalaPemeriksaan
Ribbon Anotasi tidak munculKemampuan Annotation dan empat flag CSS/script anotasi
Penyimpanan melaporkan errorToken atau kedaluwarsa sesi, dan BasePath widget
ctlDoc_Created (atau fungsi event lain) tidak pernah dijalankanNama fungsi: id penampil + _ + event, sebagai fungsi global
Anotasi C# tidak munculNomor halaman mulai dari satu, dan data dimuat ke dalam token aktif
Anotasi gambar muncul di layar tetapi tidak di eksporHost gambar memiliki alamat privat atau loopback — lihat di atas
Dokumen yang dibuka kembali tidak memiliki anotasiPersistensikan XML atau data di luar sesi penampil, lalu muat ke dalam token baru

Apakah halaman ini membantu?