주석
뷰어에 주석 지원 추가
Doconut의 주석은 두 가지 방식으로 작동합니다: 사용자가 브라우저 위젯에서 직접 그리면 서버가 페이지별로 저장하고, 또는 코드에서 주석을 생성하여 열린 세션에 로드합니다. 어느 쪽이든 페이지에 렌더링되며 PDF 또는 PNG 내보내기에 포함될 수 있습니다.
주석 지원은 Annotation 라이선스 기능에 의해 제한됩니다 (활성 임시 라이선스가 있으면 자동으로 부여됩니다).
주석 UI 활성화
주석은 Viewer 모듈이며, 독립적인 툴바가 아닙니다. 페이지에는 Viewer 리소스, Viewer 툴바, Viewer 마운트, 그리고 초기화된 objViewer가 포함되어야 합니다; 그 후 Annotation Ribbon이 마운트되어 동일한 인스턴스에 연결됩니다.
주석 파일을 Viewer 파일과 함께 배포합니다. 이 파일들은 라이선스에 따라 제한되므로, 기능이 부여될 때만 해당 태그가 나타납니다. MVC 5에서는 컨트롤러에서 태그를 생성합니다 (뷰에는 @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 // the embedded annotation ribbon
});
ViewBag.IsAnnotationEnabled = viewer.IsAnnotationEnabled;마크업에 전체 Viewer 구성을 유지합니다:
<nav id="toolbar" aria-label="Document viewer controls">
<!-- Viewer controls, including the button that opens Annotation -->
</nav>
<div id="annBarMount"></div>
<div id="divDocViewer"><div id="div_ctlDoc"></div></div>annBarMount 내부에 Annotation 파일이 Ribbon을 생성합니다; 버튼이나 대화 상자를 복사할 필요가 없습니다. 먼저 docViewer를 초기화하고, 서버가 Annotation이 라이선스가 있음을 확인할 때만 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)은 주석이 있는 페이지가 렌더링될 때 자동으로 이루어집니다.
annBar.open() 및 annBar.close()를 사용하여 자체 툴바에서 Ribbon을 열고 닫을 수 있습니다. 공개 API는 다음과 같습니다:
| 메서드 | 목적 |
|---|---|
attach(objViewer) | 초기화된 뷰어에 리본을 연결합니다; 한 번만 필요합니다 |
open() / close() | 주석 편집을 시작하거나 종료합니다 |
reset() | 리본을 닫힌 비편집 상태로 되돌립니다 |
isOpen() / annotating() | 리본 상태 / 뷰어의 주석 편집 상태를 읽습니다 |
reopenEditable() | 현재 페이지의 주석을 편집 가능한 객체로 다시 로드합니다 |
updateActionState() | 호스트 변경 후 저장 및 삭제 컨트롤을 새로 고칩니다 |
headerSlot() | 호스트가 소유한 컨트롤을 위한 선택적 헤더 슬롯 |
onStatus, onToast, onLayout, onEditStart, 및 onEditEnd는 선택적 호스트 콜백입니다. endpoints 객체는 exportPdf, exportPng, imageUpload, imageList도 제공할 수 있습니다; 구성된 엔드포인트가 없는 컨트롤은 숨겨진 상태로 유지됩니다.
두 종류의 콜백, 두 가지 방식으로 연결
주석 코드는 두 가지 콜백 스타일을 사용하며, 서로 반대로 연결하면 각각 조용히 실패합니다:
onAnnLoaded,onAnnSaved,onAnnSaveError,onAnnClosed은docViewer({...})의 옵션이며, 모든 뷰어 콜백과 같습니다.Created,Changed,Deleted,Properties은 이름으로 찾는 전역 함수이며, 뷰어 ID가 이벤트 앞에 붙습니다.#div_ctlDoc에서 뷰어 ID는ctlDoc이므로 함수 이름은ctlDoc_Created,ctlDoc_Changed,ctlDoc_Deleted,ctlDoc_Properties입니다. 오타가 있거나 이름이 바뀐 함수는 호출되지 않습니다.
SaveAnnotations() 은 저장 여부를 반환하지 않습니다: 결과는 onAnnSaved 또는 onAnnSaveError 로 전달됩니다. 반환 값을 읽는 코드는 "저장되지 않은 변경" 플래그를 절대 해제하지 않습니다.
C#에서 주석 만들기
열린 세션에 바인딩된 매니저를 가져와 주석을 추가하고 로드합니다 (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>();
// Bound to the open session's page dimensions
var manager = viewer.GetAnnotationManager(token);
var pageCount = viewer.GetPageCount(token);
// One stamp per page
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));
// Load into the session - the viewer fetches them, and the renderer burns them into
// image/PDF exports.
viewer.LoadAnnotationData(token, manager);
return new HttpStatusCodeResult(200);
}주석 유형
모든 유형은 Doconut.Annotations 에 존재하며 BaseAnnotation (페이지 번호와 경계 Rectangle)을 상속합니다:
| Type | Notes |
|---|---|
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 은 주석이 추가될 때 현재 요청의 호스트를 기준으로 해석됩니다 — 아래의 내보내기 주석을 참고하세요 |
AnnotationManager API
| Member | Purpose |
|---|---|
Add(BaseAnnotation) | 주석을 대기열에 추가 |
GetAnnotations() / GetAnnotations(int page) | 매니저가 보유한 주석을 확인 |
ClearAnnotations() / ClearAnnotations(int page) | 전체 또는 페이지별로 제거 |
GetAnnotationData() / GetAnnotationData(int page) | 인코딩된 주석 데이터 — 위젯이 사용하는 포장 형태 |
GetAnnotationXml() | XML 형태 |
Viewer 는 세션에 대한 로드 및 읽기 작업을 반영합니다: LoadAnnotationData(token, manager) 또는 LoadAnnotationData(token, encodedData), LoadAnnotationXML(token, xml), GetAnnotationXML(token).
주석이 포함된 내보내기
// PDF of all pages with annotations rendered onto them
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");
}
// Or a ZIP of per-page PNGs
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");
}내보내기는 화면 렌더링과 동일한 버너를 사용하며, 동일한 라이선스와 사용자 지정 워터마크 결정을 따릅니다.
인트라넷 호스트에서 이미지 주석을 내보낼 때 "Image blocked" 표시
뷰어와 내보내기는 ImageAnnotation의 이미지를 서로 다른 위치에서 가져옵니다:
| 구분 | 이미지를 다운로드하는 주체 | 인트라넷 또는 개발 호스트에서 |
|---|---|---|
| 브라우저의 뷰어 | 사용자의 브라우저 | 이미지가 표시됩니다 |
ExportAnnotationsToPdfAsync / …PngZipAsync | 서버 | 빨간색 "Image blocked" 자리 표시자 |
서버가 이미지를 가져와 삽입하려 할 때, 사설 네트워크로 다시 돌아가는 주소를 거부합니다: 루프백 (localhost, 127.0.0.1), 0.0.0.0, 링크‑로컬 (169.254.x.x), 사설 범위 10.x, 172.16–31.x, 192.168.x, 캐리어‑그레이드 NAT (100.64.0.0/10), 그리고 IPv6 링크‑로컬 및 유니크‑로컬 주소. 이는 조작된 이미지 URL을 통해 내부 시스템에 접근하는 것을 방지하기 위한 것이며, 이를 끄는 스위치는 존재하지 않습니다.
상대 이미지 URL은 현재 요청의 호스트를 기준으로 해석됩니다. 따라서 localhost이든, 인트라넷 서버이든, 사설 주소를 통해 접근한 사이트이든, 이미지가 뷰어에는 표시되지만 내보내기에는 자리 표시자가 들어갑니다. 이는 렌더링 오류가 아니라 보안 방어가 작동한 결과입니다. 내보내기에 이미지가 반드시 포함되어야 한다면, 공용 주소를 가진 호스트에서 제공하고 절대 URL을 사용하십시오. 전혀 해석되지 못하는 이미지 URL은 해당 주석을 오류 없이 사라지게 합니다 — 상태 코드가 아니라 렌더링된 픽셀이나 네트워크 로그를 확인하세요.
지속성 워크플로우
- 문서를 열고 토큰을 가져옵니다.
- 이전에 저장된 XML 또는 인코딩된 데이터를 해당 토큰에 로드합니다.
- 위젯이 세션 주석을 읽고 편집하도록 합니다.
- 애플리케이션이 지속을 결정할 때
GetAnnotationXML(token)으로 XML을 가져옵니다. - 플랫 형태의 결과물이 필요할 때 PDF 또는 PNG로 내보냅니다.
- 문서 세션을 종료합니다.
뷰어 토큰을 영구적인 주석 식별자로 사용하지 마십시오 — 세션이 종료되면 토큰도 사라지고, 애플리케이션 풀을 재활용하면 단일 서버의 모든 세션이 종료됩니다. 주석 데이터를 자체 문서 및 버전 식별자와 연계하여 저장하십시오.
보안 및 렌더링 참고사항
- 주석 요청은 페이지 요청과 동일한 세션 및 토큰 보안을 사용합니다.
- 사용자가 제공한 이미지 URL을 검증하고 제어합니다.
- 대용량 자유형 페이로드와 고해상도 내보내기는 메모리 사용량을 증가시킵니다; 실제 문서와 줌 값을 테스트하세요.
문제 해결
| 증상 | 확인 사항 |
|---|---|
| 주석 리본이 없습니다 | Annotation 기능 및 네 개의 주석 CSS/스크립트 플래그 |
| 저장 시 오류가 보고됩니다 | 토큰 또는 세션 만료, 그리고 위젯의 BasePath |
ctlDoc_Created(또는 다른 이벤트 함수)가 실행되지 않습니다 | 함수 이름: 뷰어 ID + _ + 이벤트, 전역 함수 형태 |
| C# 주석이 표시되지 않습니다 | 페이지 번호는 1부터 시작하며, 데이터가 활성 토큰에 로드되었습니다 |
| 이미지 주석이 화면에는 표시되지만 내보내기에는 포함되지 않습니다 | 이미지 호스트가 사설 또는 루프백 주소를 가지고 있습니다 — 위를 참고하세요 |
| 다시 연 문서에 주석이 없습니다 | 뷰어 세션 외부에 XML 또는 데이터를 지속한 뒤 새로운 토큰에 로드합니다 |
이 페이지가 도움이 되었나요?