คำอธิบาย
เพิ่มการสนับสนุนคำอธิบายในตัวดู
คำอธิบายใน Doconut ทำงานในสองทิศทาง: ผู้ใช้วาดมันในวิดเจ็ตของเบราว์เซอร์และเซิร์ฟเวอร์เก็บไว้ต่อหน้า, หรือโค้ดของคุณสร้างมันและโหลดเข้าสู่เซสชันที่เปิดอยู่. ไม่ว่าจะอย่างใดก็จะถูกแสดงบนหน้าและสามารถฝังลงในไฟล์ PDF หรือ PNG ได้.
การสนับสนุนคำอธิบายถูกจำกัดโดยความสามารถของใบอนุญาต Annotation (ได้รับโดยอัตโนมัติภายใต้ใบอนุญาต Temporary ที่ใช้งานอยู่).
เปิดใช้งาน UI ของคำอธิบาย
คำอธิบายเป็นโมดูลของ Viewer, ไม่ใช่แถบเครื่องมือแบบสแตนด์อโลน. หน้าเว็บต้องรวมทรัพยากรของ Viewer, แถบเครื่องมือของ Viewer, จุดเมานท์ของ Viewer, และ objViewer ที่ได้เริ่มต้นแล้ว; จากนั้นแถบคำอธิบาย (Annotation Ribbon) จะถูกเมานท์และเชื่อมต่อกับอินสแตนซ์เดียวกันนั้น.
ส่งออกไฟล์คำอธิบายพร้อมกับไฟล์ Viewer. ไฟล์เหล่านี้ถูกจำกัดตามใบอนุญาต, ดังนั้นแท็กของมันจะปรากฏเฉพาะเมื่อความสามารถได้รับการมอบให้. ใน MVC 5, สร้างแท็กในคอนโทรลเลอร์ (view ไม่มี @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 // แถบคำอธิบายที่ฝังอยู่
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;เก็บโครงสร้าง Viewer ทั้งหมดไว้ใน markup:
<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.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 สาธารณะของมัน:
| Method | Purpose |
|---|---|
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, andPropertiesเป็น ฟังก์ชันระดับโลกที่ค้นหาโดยใช้ชื่อ: โดยนำรหัสของ 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):
// 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 ที่ฝังอยู่
// 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 รูปภาพที่ไม่สามารถแก้ไขได้เลยจะทำให้หมายเหตุรูปภาพนั้นหายไปโดยไม่มีข้อผิดพลาด — ให้ตรวจสอบพิกเซลที่เรนเดอร์หรือบันทึกเครือข่าย ไม่ใช่รหัสสถานะ
กระบวนการคงสภาพ
- เปิดเอกสารและรับโทเค็นของมัน
- โหลด XML หรือข้อมูลที่เข้ารหัสไว้ก่อนหน้านี้เข้าสู่โทเค็นนั้น
- ให้วิดเจ็ตอ่านและแก้ไขหมายเหตุของเซสชัน
- ดึง XML ด้วย
GetAnnotationXML(token)เมื่อแอปพลิเคชันของคุณตัดสินใจบันทึก - ส่งออก PDF หรือ PNG เมื่อจำเป็นต้องมีไฟล์ที่แบน
- ปิดเซสชันเอกสาร
ห้ามใช้โทเค็นของผู้ดูเป็นตัวระบุหมายเหตุถาวร — โทเค็นจะหมดอายุพร้อมกับเซสชัน และการรีไซเคิลของ application pool จะทำให้เซสชันทั้งหมดบนเซิร์ฟเวอร์เดียวสิ้นสุด เก็บข้อมูลหมายเหตุไว้กับเอกสารและตัวระบุเวอร์ชันของคุณเอง
หมายเหตุด้านความปลอดภัยและการเรนเดอร์
- คำขอหมายเหตุใช้การรักษาความปลอดภัยของเซสชันและโทเค็นเดียวกับคำขอหน้า
- ตรวจสอบและควบคุม URL รูปภาพที่ผู้ใช้ให้มา
- ข้อมูลฟรีแฮนด์ขนาดใหญ่และการส่งออกความละเอียดสูงจะเพิ่มการใช้หน่วยความจำ; ทดสอบเอกสารที่เป็นจริงและค่าการซูม
การแก้ไขปัญหา
| อาการ | ตรวจสอบ |
|---|---|
| ริบบอนหมายเหตุหายไป | ความสามารถ Annotation และสัญญาณ CSS/สคริปต์หมายเหตุสี่ตัว |
| การบันทึกรายงานข้อผิดพลาด | โทเค็นหรือเซสชันหมดอายุ, และ BasePath ของวิดเจ็ต |
ctlDoc_Created (หรือฟังก์ชันเหตุการณ์อื่น) ไม่เคยทำงาน | ชื่อฟังก์ชัน: viewer id + _ + event, เป็นฟังก์ชันระดับโลก |
| หมายเหตุ C# ไม่ปรากฏ | หมายเลขหน้าเริ่มจาก 1, และข้อมูลถูกโหลดเข้าสู่โทเค็นที่ใช้งาน |
| หมายเหตุรูปภาพแสดงบนหน้าจอแต่ไม่อยู่ในไฟล์ส่งออก | โฮสต์ของรูปภาพมีที่อยู่ส่วนตัวหรือ loopback — ดูด้านบน |
| เอกสารที่เปิดใหม่ไม่มีหมายเหตุ | บันทึก XML หรือข้อมูลนอกเซสชันผู้ดู, แล้วโหลดเข้าสู่โทเค็นใหม่ |
หน้านี้เป็นประโยชน์หรือไม่?