คำอธิบาย

เพิ่มการสนับสนุนคำอธิบายในตัวดู

คำอธิบายใน Doconut ทำงานในสองทิศทาง: ผู้ใช้วาดมันในวิดเจ็ตของเบราว์เซอร์และเซิร์ฟเวอร์เก็บไว้ต่อหน้า, หรือโค้ดของคุณสร้างมันและโหลดเข้าสู่เซสชันที่เปิดอยู่. ไม่ว่าจะอย่างใดก็จะถูกแสดงบนหน้าและสามารถฝังลงในไฟล์ PDF หรือ PNG ได้.

การสนับสนุนคำอธิบายถูกจำกัดโดยความสามารถของใบอนุญาต Annotation (ได้รับโดยอัตโนมัติภายใต้ใบอนุญาต Temporary ที่ใช้งานอยู่).

เปิดใช้งาน UI ของคำอธิบาย

คำอธิบายเป็นโมดูลของ Viewer, ไม่ใช่แถบเครื่องมือแบบสแตนด์อโลน. หน้าเว็บต้องรวมทรัพยากรของ Viewer, แถบเครื่องมือของ Viewer, จุดเมานท์ของ Viewer, และ objViewer ที่ได้เริ่มต้นแล้ว; จากนั้นแถบคำอธิบาย (Annotation Ribbon) จะถูกเมานท์และเชื่อมต่อกับอินสแตนซ์เดียวกันนั้น.

ส่งออกไฟล์คำอธิบายพร้อมกับไฟล์ Viewer. ไฟล์เหล่านี้ถูกจำกัดตามใบอนุญาต, ดังนั้นแท็กของมันจะปรากฏเฉพาะเมื่อความสามารถได้รับการมอบให้. ใน MVC 5, สร้างแท็กในคอนโทรลเลอร์ (view ไม่มี @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   // แถบคำอธิบายที่ฝังอยู่
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;

เก็บโครงสร้าง Viewer ทั้งหมดไว้ใน markup:

html
<nav id="toolbar" aria-label="Document viewer controls">
    <!-- ควบคุมตัวดู, รวมถึงปุ่มที่เปิดคำอธิบาย -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>

ไฟล์คำอธิบายจะสร้าง Ribbon ภายใน annBarMount; คุณไม่ต้องคัดลอกปุ่มหรือไดอะล็อกของมัน. เริ่มต้น docViewer ก่อน, จากนั้นสร้าง Ribbon เฉพาะเมื่อเซิร์ฟเวอร์ยืนยันว่าคำอธิบายได้รับใบอนุญาต:

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>

หากไม่มีความสามารถ, ReferenceScripts จะไม่รวมสคริปต์ของ Ribbon, ดังนั้นเงื่อนไขฝั่งเซิร์ฟเวอร์ if จะป้องกันการเรียก doconutAnnotationBar(...) ที่ไม่ได้รับการคุ้มครองจากการทำให้หน้าเสียหาย. ใน Web Forms, เขียนการป้องกันเดียวกันเป็น <% if (IsAnnotationEnabled) { %> … <% } %>.

การบันทึกจาก Ribbon จะโพสต์ข้อมูลไปยัง DocImage.axd (AnnSave), ซึ่งจะเก็บไว้ในเซสชันเอกสารต่อหน้า. การโหลด (AnnLoad) จะเกิดขึ้นโดยอัตโนมัติเมื่อหน้าที่มีคำอธิบายถูกเรนเดอร์.

เปิดและปิด Ribbon จากแถบเครื่องมือของคุณเองด้วย annBar.open() และ annBar.close(). API สาธารณะของมัน:

MethodPurpose
attach(objViewer)เชื่อมต่อ Ribbon กับ Viewer ที่ได้เริ่มต้นแล้ว; ต้องทำครั้งเดียว
open() / close()เข้าหรือออกจากการแก้ไขคำอธิบาย
reset()คืนค่า Ribbon ให้เป็นสถานะปิดและไม่อยู่ในโหมดแก้ไข
isOpen() / annotating()อ่านสถานะของ Ribbon / สถานะการแก้ไขคำอธิบายของ Viewer
reopenEditable()โหลดคำอธิบายของหน้าปัจจุบันใหม่เป็นออบเจ็กต์ที่แก้ไขได้
updateActionState()รีเฟรชการควบคุมบันทึกและลบหลังจากโฮสต์มีการเปลี่ยนแปลง
headerSlot()ช่องหัวเรื่องทางเลือกสำหรับการควบคุมที่เป็นของโฮสต์

onStatus, onToast, onLayout, onEditStart, และ onEditEnd เป็นคอลแบ็กของโฮสต์ที่เป็นตัวเลือก. อ็อบเจ็กต์ endpoints สามารถให้ exportPdf, exportPng, imageUpload, และ imageList ได้เช่นกัน; ควบคุมที่ไม่มี endpoint ที่กำหนดจะถูกซ่อน.

สองประเภทของ callback ที่เชื่อมต่อสองวิธี

โค้ด annotation ใช้รูปแบบ callback สองแบบ และแต่ละแบบจะล้มเหลวโดยเงียบเมื่อเชื่อมต่อในทิศทางตรงกันข้าม:

  • onAnnLoaded, onAnnSaved, onAnnSaveError, onAnnClosed เป็น options ของ docViewer({...}) เช่น callback ของ viewer ทุกตัว
  • Created, Changed, Deleted, and Properties เป็น ฟังก์ชันระดับโลกที่ค้นหาโดยใช้ชื่อ: โดยนำรหัสของ viewer มาต่อหน้าชื่อเหตุการณ์ ตัวอย่างสำหรับ viewer ที่อยู่ใน #div_ctlDoc รหัสคือ ctlDoc ดังนั้นฟังก์ชันจะเป็น ctlDoc_Created, ctlDoc_Changed, ctlDoc_Deleted, และ ctlDoc_Properties. ฟังก์ชันที่สะกดผิดหรือเปลี่ยนชื่อจะไม่ถูกเรียกใช้เลย

SaveAnnotations() ไม่ได้คืนค่าว่าบันทึกสำเร็จหรือไม่: ผลลัพธ์จะมาถึงผ่าน onAnnSaved หรือ onAnnSaveError โค้ดที่อ่านค่าที่คืนมาจะไม่เคลียร์แฟล็ก “การเปลี่ยนแปลงที่ยังไม่ได้บันทึก”

สร้าง annotation ด้วย C#

รับ manager ที่ผูกกับเซสชันที่เปิดอยู่, เพิ่ม annotation, แล้วโหลดมัน (โดยใช้ using Doconut.Annotations; สำหรับประเภทและ using System.Drawing; สำหรับ Rectangle และ Color):

csharp
// POST /Annotations/LoadSample?token=...
[HttpPost]
public ActionResult LoadSample(string token)
{
    var viewer = Doconut.DoconutHost.Services.GetRequiredService<Viewer>();

    // ผูกกับขนาดหน้าของเซสชันที่เปิดอยู่
    var manager   = viewer.GetAnnotationManager(token);
    var pageCount = viewer.GetPageCount(token);

    // สต็ัมป์หนึ่งอันต่อหน้า
    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));

    // โหลดเข้าสู่เซสชัน - viewer ดึงข้อมูลและ renderer ฝังลงใน
    // การส่งออกเป็น image/PDF.
    viewer.LoadAnnotationData(token, manager);
    return new HttpStatusCodeResult(200);
}

ประเภทของ Annotation

ทุกประเภทอยู่ใน Doconut.Annotations และสืบทอดจาก BaseAnnotation (หมายเลขหน้าและ Rectangle ที่กำหนดขอบเขต):

Typeหมายเหตุ
StampAnnotationสต็ัมป์ข้อความพร้อมขนาดฟอนต์, เส้นขอบ, สี; รองรับ Opacity, Rotate
NoteAnnotationโน้ตแบบสติ๊กกี้พร้อมข้อความ, สีพื้นหลัง, ขนาดฟอนต์, TitleColor
RectangleAnnotationสีเส้นขอบและสีเติม, Title/ShowTitle
CircleAnnotationเส้นขอบและสีเติม, ShowBorder
EllipseAnnotationเส้นขอบและสีเติม, ShowBorder
TriangleAnnotationสีเส้นขอบ, BackColor, ShowBorder
LineAnnotationเส้นตรงพร้อมความกว้างและสี
ArrowAnnotationเส้นพร้อมหัวศร; สามารถตั้งค่า Direction (ArrowDirection, จุดทิศทาง, ค่าเริ่มต้น E)
FreehandAnnotationเส้นวาดอิสระจากจุด FreehandData ที่เข้ารหัส
ImageAnnotationรูปภาพจาก URL. URL relative จะถูกแก้ไขโดยอิงจากโฮสต์ของคำขอปัจจุบันเมื่อเพิ่ม annotation — ดูหมายเหตุการส่งออกด้านล่าง

API ของ AnnotationManager

Memberวัตถุประสงค์
Add(BaseAnnotation)เพิ่ม annotation ลงในคิว
GetAnnotations() / GetAnnotations(int page)ตรวจสอบสิ่งที่ manager มีอยู่
ClearAnnotations() / ClearAnnotations(int page)ลบทั้งหมด / ตามหน้า
GetAnnotationData() / GetAnnotationData(int page)ข้อมูล annotation ที่เข้ารหัส — ซองที่ widget ใช้
GetAnnotationXml()รูปแบบ XML

Viewer ทำสำเนาการโหลดและการอ่านข้อมูลต่อเซสชัน: LoadAnnotationData(token, manager) หรือ LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), และ GetAnnotationXML(token).

ส่งออกพร้อม annotation ที่ฝังอยู่

csharp
// PDF ของทุกหน้าที่มี annotation แสดงบนหน้า
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");
}

// หรือ ZIP ของ PNG แยกตามหน้า
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");
}

การส่งออกใช้กระบวนการฝังเดียวกับการเรนเดอร์บนหน้าจอ, และใช้ใบอนุญาตและการตัดสินใจเกี่ยวกับลายน้ำแบบกำหนดเองเดียวกัน

การทำหมายเหตุรูปภาพบนโฮสต์อินทราเน็ตส่งออกเป็น “ภาพถูกบล็อก”

ผู้ดูและกระบวนการส่งออกดึงรูปภาพของ ImageAnnotation จากแหล่งที่ต่างกัน:

พื้นผิวผู้ที่ดาวน์โหลดรูปภาพบนโฮสต์อินทราเน็ตหรือโฮสต์การพัฒนา
ผู้ดูในเบราว์เซอร์เบราว์เซอร์ของผู้ใช้รูปภาพปรากฏ
ExportAnnotationsToPdfAsync / …PngZipAsyncเซิร์ฟเวอร์ตัวแทนสีแดง “ภาพถูกบล็อก”

เมื่อเซิร์ฟเวอร์ดึงรูปภาพเพื่อฝังลงไป มันจะปฏิเสธที่อยู่ที่ชี้กลับไปยังเครือข่ายส่วนตัว: loopback (localhost, 127.0.0.1), 0.0.0.0, link‑local (169.254.x.x), ช่วงที่อยู่ส่วนตัว 10.x, 172.16–31.x และ 192.168.x, carrier‑grade NAT (100.64.0.0/10), รวมถึงที่อยู่ IPv6 link‑local และ unique‑local การป้องกันนี้ช่วยไม่ให้เซิร์ฟเวอร์ถูกใช้เพื่อเข้าถึงระบบภายในผ่าน URL รูปภาพที่จัดทำขึ้นโดยเจตนา และไม่มีสวิตช์ใดให้ปิดการป้องกันนี้

URL รูปภาพแบบ relative จะถูกแก้ไขโดยอิงจากโฮสต์ของคำขอปัจจุบัน ดังนั้นบน localhost, บนเซิร์ฟเวอร์อินทราเน็ต หรือบนไซต์ใด ๆ ที่เข้าถึงผ่านที่อยู่ส่วนตัว รูปภาพจะแสดงในผู้ดูและไฟล์ส่งออกจะมีตัวแทนแทนที่แทนที่จะเป็นรูปภาพจริง นี่คือการทำงานของการป้องกัน ไม่ใช่ความล้มเหลวของการเรนเดอร์ หากต้องการให้ไฟล์ส่งออกมีรูปภาพจริง ให้ให้บริการรูปภาพจากโฮสต์ที่มีที่อยู่สาธารณะและใช้ URL แบบ absolute URL URL รูปภาพที่ไม่สามารถแก้ไขได้เลยจะทำให้หมายเหตุรูปภาพนั้นหายไปโดยไม่มีข้อผิดพลาด — ให้ตรวจสอบพิกเซลที่เรนเดอร์หรือบันทึกเครือข่าย ไม่ใช่รหัสสถานะ

กระบวนการคงสภาพ

  1. เปิดเอกสารและรับโทเค็นของมัน
  2. โหลด XML หรือข้อมูลที่เข้ารหัสไว้ก่อนหน้านี้เข้าสู่โทเค็นนั้น
  3. ให้วิดเจ็ตอ่านและแก้ไขหมายเหตุของเซสชัน
  4. ดึง XML ด้วย GetAnnotationXML(token) เมื่อแอปพลิเคชันของคุณตัดสินใจบันทึก
  5. ส่งออก PDF หรือ PNG เมื่อจำเป็นต้องมีไฟล์ที่แบน
  6. ปิดเซสชันเอกสาร

ห้ามใช้โทเค็นของผู้ดูเป็นตัวระบุหมายเหตุถาวร — โทเค็นจะหมดอายุพร้อมกับเซสชัน และการรีไซเคิลของ application pool จะทำให้เซสชันทั้งหมดบนเซิร์ฟเวอร์เดียวสิ้นสุด เก็บข้อมูลหมายเหตุไว้กับเอกสารและตัวระบุเวอร์ชันของคุณเอง

หมายเหตุด้านความปลอดภัยและการเรนเดอร์

  • คำขอหมายเหตุใช้การรักษาความปลอดภัยของเซสชันและโทเค็นเดียวกับคำขอหน้า
  • ตรวจสอบและควบคุม URL รูปภาพที่ผู้ใช้ให้มา
  • ข้อมูลฟรีแฮนด์ขนาดใหญ่และการส่งออกความละเอียดสูงจะเพิ่มการใช้หน่วยความจำ; ทดสอบเอกสารที่เป็นจริงและค่าการซูม

การแก้ไขปัญหา

อาการตรวจสอบ
ริบบอนหมายเหตุหายไปความสามารถ Annotation และสัญญาณ CSS/สคริปต์หมายเหตุสี่ตัว
การบันทึกรายงานข้อผิดพลาดโทเค็นหรือเซสชันหมดอายุ, และ BasePath ของวิดเจ็ต
ctlDoc_Created (หรือฟังก์ชันเหตุการณ์อื่น) ไม่เคยทำงานชื่อฟังก์ชัน: viewer id + _ + event, เป็นฟังก์ชันระดับโลก
หมายเหตุ C# ไม่ปรากฏหมายเลขหน้าเริ่มจาก 1, และข้อมูลถูกโหลดเข้าสู่โทเค็นที่ใช้งาน
หมายเหตุรูปภาพแสดงบนหน้าจอแต่ไม่อยู่ในไฟล์ส่งออกโฮสต์ของรูปภาพมีที่อยู่ส่วนตัวหรือ loopback — ดูด้านบน
เอกสารที่เปิดใหม่ไม่มีหมายเหตุบันทึก XML หรือข้อมูลนอกเซสชันผู้ดู, แล้วโหลดเข้าสู่โทเค็นใหม่

หน้านี้เป็นประโยชน์หรือไม่?