mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1 parent
0322995771
commit
677396c71d
12 files changed
+377
-64
No files matched your search
+78
-15
@@ -30,6 +30,8 @@ const STATUS = {
|
|||||||
};
|
};
|
||||||
const WASM_CALLBACKS = [
|
const WASM_CALLBACKS = [
|
||||||
"ofdgoOpen",
|
"ofdgoOpen",
|
||||||
|
"ofdgoConfigure",
|
||||||
|
"ofdgoDocumentInfo",
|
||||||
"ofdgoRenderPage",
|
"ofdgoRenderPage",
|
||||||
"ofdgoExportFormats",
|
"ofdgoExportFormats",
|
||||||
"ofdgoExportPage",
|
"ofdgoExportPage",
|
||||||
@@ -66,10 +68,12 @@ const state = {
|
|||||||
pageRenderQueue: [],
|
pageRenderQueue: [],
|
||||||
pageRenderRunning: false,
|
pageRenderRunning: false,
|
||||||
pageObserver: null,
|
pageObserver: null,
|
||||||
|
visiblePages: new Set(),
|
||||||
scrollFrame: 0,
|
scrollFrame: 0,
|
||||||
thumbnailCache: new Map(),
|
thumbnailCache: new Map(),
|
||||||
thumbnailInFlight: new Set(),
|
thumbnailInFlight: new Set(),
|
||||||
thumbnailObserver: null,
|
thumbnailObserver: null,
|
||||||
|
visibleThumbnails: new Set(),
|
||||||
exportFormats: [],
|
exportFormats: [],
|
||||||
showPages: !COMPACT_LAYOUT.matches,
|
showPages: !COMPACT_LAYOUT.matches,
|
||||||
showMeta: !COMPACT_LAYOUT.matches,
|
showMeta: !COMPACT_LAYOUT.matches,
|
||||||
@@ -725,6 +729,8 @@ async function applyFontChange(options = {}) {
|
|||||||
pageIndex: state.pageIndex,
|
pageIndex: state.pageIndex,
|
||||||
fitMode: state.fitMode,
|
fitMode: state.fitMode,
|
||||||
skipAutoFonts: options.skipAutoFonts !== false,
|
skipAutoFonts: options.skipAutoFonts !== false,
|
||||||
|
reuseSession: true,
|
||||||
|
fontsChanged: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,6 +744,7 @@ async function toggleAnnotations() {
|
|||||||
pageIndex: state.pageIndex,
|
pageIndex: state.pageIndex,
|
||||||
fitMode: state.fitMode,
|
fitMode: state.fitMode,
|
||||||
skipAutoFonts: true,
|
skipAutoFonts: true,
|
||||||
|
reuseSession: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,7 +891,10 @@ async function openDocument(options = {}) {
|
|||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const doc = callWASM("ofdgoOpen", state.ofdBytes, resetLocalFonts ? uploadedFonts() : allFonts(), state.renderAnnotations);
|
const fonts = resetLocalFonts ? uploadedFonts() : allFonts();
|
||||||
|
const doc = options.reuseSession
|
||||||
|
? callWASM("ofdgoConfigure", options.fontsChanged ? fonts : null, state.renderAnnotations)
|
||||||
|
: callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -904,6 +914,8 @@ async function openDocument(options = {}) {
|
|||||||
resetScroll: options.resetScroll,
|
resetScroll: options.resetScroll,
|
||||||
skipAutoFonts: true,
|
skipAutoFonts: true,
|
||||||
openSeq,
|
openSeq,
|
||||||
|
reuseSession: true,
|
||||||
|
fontsChanged: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -926,6 +938,7 @@ async function openDocument(options = {}) {
|
|||||||
await nextFrame();
|
await nextFrame();
|
||||||
await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
|
await renderPage(pageIndex, { keepBusy: true, scroll: false, openSeq });
|
||||||
queueNearbyPages(pageIndex, openSeq);
|
queueNearbyPages(pageIndex, openSeq);
|
||||||
|
loadDocumentDetails(openSeq);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
@@ -938,6 +951,24 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDocumentDetails(openSeq) {
|
||||||
|
await waitForPaint();
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const info = callWASM("ofdgoDocumentInfo");
|
||||||
|
if (openSeq === state.openSeq) {
|
||||||
|
Object.assign(state.doc, info, { detailsPending: false });
|
||||||
|
renderMeta();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (openSeq === state.openSeq) {
|
||||||
|
showError(err, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function renderPage(index, options = {}) {
|
async function renderPage(index, options = {}) {
|
||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
return;
|
return;
|
||||||
@@ -1100,6 +1131,7 @@ function renderPageFlow() {
|
|||||||
|
|
||||||
function resetPageFlow() {
|
function resetPageFlow() {
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
|
state.visiblePages.clear();
|
||||||
for (const task of state.pageInFlight.values()) {
|
for (const task of state.pageInFlight.values()) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
}
|
}
|
||||||
@@ -1116,7 +1148,6 @@ function observeFlowPage(shell, index) {
|
|||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
if (index < 4 && index !== state.pageIndex) {
|
if (index < 4 && index !== state.pageIndex) {
|
||||||
renderFlowPage(index, { openSeq, priority: 3 });
|
renderFlowPage(index, { openSeq, priority: 3 });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const observer = flowPageObserver();
|
const observer = flowPageObserver();
|
||||||
if (observer) {
|
if (observer) {
|
||||||
@@ -1131,12 +1162,20 @@ function flowPageObserver() {
|
|||||||
if (!state.pageObserver) {
|
if (!state.pageObserver) {
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
state.pageObserver = new IntersectionObserver((entries) => {
|
state.pageObserver = new IntersectionObserver((entries) => {
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
|
||||||
if (!entry.isIntersecting) {
|
if (!entry.isIntersecting) {
|
||||||
|
state.visiblePages.delete(index);
|
||||||
|
if (entry.target.classList.contains("rendered")) {
|
||||||
|
entry.target.querySelector(".page-surface").replaceChildren();
|
||||||
|
entry.target.classList.remove("rendered");
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
|
state.visiblePages.add(index);
|
||||||
state.pageObserver.unobserve(entry.target);
|
|
||||||
renderFlowPage(index, { openSeq, priority: 4 });
|
renderFlowPage(index, { openSeq, priority: 4 });
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@@ -1153,13 +1192,13 @@ async function renderFlowPage(index, options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const page = await loadPageData(index, { openSeq, priority: options.priority || 2 });
|
const page = await loadPageData(index, { openSeq, priority: options.priority ?? 2 });
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (page) {
|
if (page) {
|
||||||
const shell = pageShell(index);
|
const shell = pageShell(index);
|
||||||
if (!shell?.classList.contains("rendered")) {
|
if (!shell?.classList.contains("rendered") && (!state.pageObserver || state.visiblePages.has(index) || index === state.pageIndex)) {
|
||||||
mountPageSVG(index, page, openSeq);
|
mountPageSVG(index, page, openSeq);
|
||||||
}
|
}
|
||||||
updateThumbnail(index, openSeq);
|
updateThumbnail(index, openSeq);
|
||||||
@@ -1185,7 +1224,7 @@ function loadPageData(index, options = {}) {
|
|||||||
return Promise.resolve(state.pageCache.get(index));
|
return Promise.resolve(state.pageCache.get(index));
|
||||||
}
|
}
|
||||||
const key = `${openSeq}:${index}`;
|
const key = `${openSeq}:${index}`;
|
||||||
const priority = options.priority || 3;
|
const priority = options.priority ?? 3;
|
||||||
const current = state.pageInFlight.get(key);
|
const current = state.pageInFlight.get(key);
|
||||||
if (current) {
|
if (current) {
|
||||||
current.priority = Math.min(current.priority, priority);
|
current.priority = Math.min(current.priority, priority);
|
||||||
@@ -1526,6 +1565,7 @@ function renderPageList() {
|
|||||||
|
|
||||||
function resetThumbnails() {
|
function resetThumbnails() {
|
||||||
state.thumbnailCache.clear();
|
state.thumbnailCache.clear();
|
||||||
|
state.visibleThumbnails.clear();
|
||||||
state.thumbnailInFlight.clear();
|
state.thumbnailInFlight.clear();
|
||||||
if (state.thumbnailObserver) {
|
if (state.thumbnailObserver) {
|
||||||
state.thumbnailObserver.disconnect();
|
state.thumbnailObserver.disconnect();
|
||||||
@@ -1540,6 +1580,11 @@ function cacheThumbnail(index, svgText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setThumbnailContent(container, svgText, index, openSeq = state.openSeq) {
|
function setThumbnailContent(container, svgText, index, openSeq = state.openSeq) {
|
||||||
|
const renderKey = `${openSeq}:${index}`;
|
||||||
|
if (svgText && container.dataset.renderKey === renderKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete container.dataset.renderKey;
|
||||||
container.replaceChildren();
|
container.replaceChildren();
|
||||||
if (!svgText) {
|
if (!svgText) {
|
||||||
container.classList.add("pending");
|
container.classList.add("pending");
|
||||||
@@ -1552,6 +1597,7 @@ function setThumbnailContent(container, svgText, index, openSeq = state.openSeq)
|
|||||||
svg.setAttribute("aria-hidden", "true");
|
svg.setAttribute("aria-hidden", "true");
|
||||||
container.classList.remove("pending", "error");
|
container.classList.remove("pending", "error");
|
||||||
container.append(svg);
|
container.append(svg);
|
||||||
|
container.dataset.renderKey = renderKey;
|
||||||
} catch {
|
} catch {
|
||||||
container.classList.add("error");
|
container.classList.add("error");
|
||||||
container.textContent = String(index + 1);
|
container.textContent = String(index + 1);
|
||||||
@@ -1559,16 +1605,15 @@ function setThumbnailContent(container, svgText, index, openSeq = state.openSeq)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function observeThumbnail(button, index, openSeq = state.openSeq) {
|
function observeThumbnail(button, index, openSeq = state.openSeq) {
|
||||||
if (state.thumbnailCache.has(index)) {
|
const observer = thumbnailObserver();
|
||||||
return;
|
if (observer) {
|
||||||
|
observer.observe(button);
|
||||||
}
|
}
|
||||||
if (index === state.pageIndex || index < 6) {
|
if (index === state.pageIndex || index < 6) {
|
||||||
renderThumbnail(index, openSeq);
|
renderThumbnail(index, openSeq);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const observer = thumbnailObserver();
|
|
||||||
if (observer) {
|
if (observer) {
|
||||||
observer.observe(button);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (index < 8) {
|
if (index < 8) {
|
||||||
@@ -1583,12 +1628,21 @@ function thumbnailObserver() {
|
|||||||
if (!state.thumbnailObserver) {
|
if (!state.thumbnailObserver) {
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
state.thumbnailObserver = new IntersectionObserver((entries) => {
|
state.thumbnailObserver = new IntersectionObserver((entries) => {
|
||||||
|
if (openSeq !== state.openSeq) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
const index = Number.parseInt(entry.target.dataset.pageIndex, 10);
|
||||||
if (!entry.isIntersecting) {
|
if (!entry.isIntersecting) {
|
||||||
|
state.visibleThumbnails.delete(index);
|
||||||
|
const thumb = entry.target.querySelector(".thumb-paper");
|
||||||
|
if (thumb.dataset.renderKey) {
|
||||||
|
setThumbnailContent(thumb, "", index, openSeq);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
state.thumbnailObserver.unobserve(entry.target);
|
state.visibleThumbnails.add(index);
|
||||||
renderThumbnail(Number.parseInt(entry.target.dataset.pageIndex, 10), openSeq);
|
renderThumbnail(index, openSeq);
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
root: el.pageListPanel,
|
root: el.pageListPanel,
|
||||||
@@ -1599,7 +1653,11 @@ function thumbnailObserver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderThumbnail(index, openSeq = state.openSeq) {
|
async function renderThumbnail(index, openSeq = state.openSeq) {
|
||||||
if (openSeq !== state.openSeq || !state.doc || state.thumbnailCache.has(index)) {
|
if (openSeq !== state.openSeq || !state.doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.thumbnailCache.has(index)) {
|
||||||
|
updateThumbnail(index, openSeq);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = `${openSeq}:${index}`;
|
const key = `${openSeq}:${index}`;
|
||||||
@@ -1626,6 +1684,9 @@ async function renderThumbnail(index, openSeq = state.openSeq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateThumbnail(index, openSeq = state.openSeq) {
|
function updateThumbnail(index, openSeq = state.openSeq) {
|
||||||
|
if (openSeq !== state.openSeq || (state.thumbnailObserver && !state.visibleThumbnails.has(index) && index !== state.pageIndex)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const thumb = el.pageList.querySelector(`[data-page-index="${index}"] .thumb-paper`);
|
const thumb = el.pageList.querySelector(`[data-page-index="${index}"] .thumb-paper`);
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
setThumbnailContent(thumb, state.thumbnailCache.get(index), index, openSeq);
|
setThumbnailContent(thumb, state.thumbnailCache.get(index), index, openSeq);
|
||||||
@@ -1647,7 +1708,7 @@ function renderMeta() {
|
|||||||
el.metaVersion.textContent = doc.version || "-";
|
el.metaVersion.textContent = doc.version || "-";
|
||||||
el.metaType.textContent = doc.docType || "-";
|
el.metaType.textContent = doc.docType || "-";
|
||||||
el.metaFonts.textContent = String(doc.fontCount || 0);
|
el.metaFonts.textContent = String(doc.fontCount || 0);
|
||||||
el.metaSignatures.textContent = String(doc.signatureCount || 0);
|
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : String(doc.signatureCount || 0);
|
||||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||||
renderSignatures();
|
renderSignatures();
|
||||||
renderDocumentFonts();
|
renderDocumentFonts();
|
||||||
@@ -1948,6 +2009,8 @@ function pageStatus(index, pageCount) {
|
|||||||
|
|
||||||
function statusText(status) {
|
function statusText(status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
case "pending":
|
||||||
|
return "待检查";
|
||||||
case "embedded":
|
case "embedded":
|
||||||
return "内嵌";
|
return "内嵌";
|
||||||
case "matched":
|
case "matched":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.26.0
|
|||||||
require (
|
require (
|
||||||
github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7
|
github.com/tdewolff/canvas v0.0.0-20260901160717-dae8cd8e19a7
|
||||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
|
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
|
||||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059
|
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67
|
||||||
golang.org/x/image v0.45.0
|
golang.org/x/image v0.45.0
|
||||||
golang.org/x/text v0.41.0
|
golang.org/x/text v0.41.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E=
|
||||||
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ=
|
||||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059 h1:/JXxWbNTePSVfshS7BVRm75Kh1/5rixvLpgTRw7MMv8=
|
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67 h1:tUpMiGYu6SQ8W1aHxAc/NGMmd0N0IqfvcaoE14q3WeQ=
|
||||||
github.com/xiaoqidun/jbig2 v0.0.0-20260823111534-2e67bcec7059/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
|
github.com/xiaoqidun/jbig2 v0.0.0-20260907061036-a2fe11801f67/go.mod h1:mdoUQnVPIA4yS0/+ZrtxJNiDdbphI0RMu+DPfxXfVhE=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type apiResult struct {
|
|||||||
// RunWASM 注册浏览器WASM接口并阻塞运行
|
// RunWASM 注册浏览器WASM接口并阻塞运行
|
||||||
func RunWASM() {
|
func RunWASM() {
|
||||||
registerCallback("ofdgoOpen", openDocument)
|
registerCallback("ofdgoOpen", openDocument)
|
||||||
|
registerCallback("ofdgoConfigure", configureDocument)
|
||||||
|
registerCallback("ofdgoDocumentInfo", documentInfo)
|
||||||
registerCallback("ofdgoRenderPage", renderPage)
|
registerCallback("ofdgoRenderPage", renderPage)
|
||||||
registerCallback("ofdgoExportFormats", exportFormats)
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
registerCallback("ofdgoExportPage", exportPage)
|
registerCallback("ofdgoExportPage", exportPage)
|
||||||
@@ -102,6 +104,36 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
currentSession = session
|
currentSession = session
|
||||||
|
return currentSession.Summary(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureDocument 更新当前会话的字体和注解设置
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 文档信息, error 错误信息
|
||||||
|
func configureDocument(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
if !args[0].IsNull() && !args[0].IsUndefined() {
|
||||||
|
fonts, err := fontsFromJS(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := currentSession.SetFonts(fonts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentSession.Renderer.RenderAnnotations = args[1].Bool()
|
||||||
|
return currentSession.Summary(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentInfo 获取首屏之后加载的字体统计和验签结果
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 文档信息, error 错误信息
|
||||||
|
func documentInfo(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
return currentSession.Info(), nil
|
return currentSession.Info(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+87
-7
@@ -94,11 +94,14 @@ type SignatureStampInfo struct {
|
|||||||
|
|
||||||
// Session WebUI文档会话
|
// Session WebUI文档会话
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Reader *ofdgo.Reader
|
Reader *ofdgo.Reader
|
||||||
Renderer *ofdgo.Renderer
|
Renderer *ofdgo.Renderer
|
||||||
doc *ofdgo.Document
|
doc *ofdgo.Document
|
||||||
pageCache map[int]*ofdgo.PageContent
|
pageCache map[int]*ofdgo.PageContent
|
||||||
boxCache map[int]ofdgo.Box
|
boxCache map[int]ofdgo.Box
|
||||||
|
signatures []SignatureInfo
|
||||||
|
signatureError error
|
||||||
|
signaturesRead bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentInfo 文档信息
|
// DocumentInfo 文档信息
|
||||||
@@ -117,6 +120,7 @@ type DocumentInfo struct {
|
|||||||
Fonts []FontInfo `json:"fonts"`
|
Fonts []FontInfo `json:"fonts"`
|
||||||
Signatures []SignatureInfo `json:"signatures"`
|
Signatures []SignatureInfo `json:"signatures"`
|
||||||
Pages []PageInfo `json:"pages"`
|
Pages []PageInfo `json:"pages"`
|
||||||
|
DetailsPending bool `json:"detailsPending,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageInfo 页面信息
|
// PageInfo 页面信息
|
||||||
@@ -222,7 +226,69 @@ func (s *Session) Close() error {
|
|||||||
return s.Reader.Close()
|
return s.Reader.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info 获取文档信息
|
// SetFonts 更新字体配置并保留文档、页面和验签结果
|
||||||
|
// 入参: fonts 字体文件列表
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (s *Session) SetFonts(fonts []FontFile) error {
|
||||||
|
opts := []ofdgo.RendererOption{
|
||||||
|
ofdgo.WithDPI(s.Renderer.DPI),
|
||||||
|
ofdgo.WithAnnotations(s.Renderer.RenderAnnotations),
|
||||||
|
}
|
||||||
|
if len(fonts) > 0 {
|
||||||
|
fontFS := ofdgo.NewFontFS(fonts)
|
||||||
|
if fontFS.Len() == 0 {
|
||||||
|
return fmt.Errorf("invalid font file")
|
||||||
|
}
|
||||||
|
opts = append(opts, ofdgo.WithFontFS(fontFS))
|
||||||
|
}
|
||||||
|
s.Renderer = ofdgo.NewRenderer(s.Reader, opts...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary 获取首屏所需信息,不解析页面图元或执行验签
|
||||||
|
// 返回: DocumentInfo 文档信息
|
||||||
|
func (s *Session) Summary() DocumentInfo {
|
||||||
|
info := DocumentInfo{
|
||||||
|
Version: s.Reader.Version(),
|
||||||
|
DocType: s.Reader.DocType(),
|
||||||
|
PageCount: len(s.doc.Pages.Page),
|
||||||
|
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
|
||||||
|
DetailsPending: true,
|
||||||
|
}
|
||||||
|
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||||
|
info.Title = docInfo.Title
|
||||||
|
info.Author = docInfo.Author
|
||||||
|
info.Subject = docInfo.Subject
|
||||||
|
info.CreationDate = docInfo.CreationDate
|
||||||
|
info.ModDate = docInfo.ModDate
|
||||||
|
}
|
||||||
|
for index, page := range s.doc.Pages.Page {
|
||||||
|
box, ok := s.boxCache[index]
|
||||||
|
if !ok {
|
||||||
|
if area, err := s.Reader.PageArea(page); err == nil {
|
||||||
|
box, _ = s.pageBox(index, &ofdgo.PageContent{Area: area})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.Pages = append(info.Pages, PageInfo{Index: index, ID: page.ID, Width: box.W, Height: box.H})
|
||||||
|
}
|
||||||
|
if fonts, err := s.Reader.Fonts(); err == nil {
|
||||||
|
for _, font := range fonts {
|
||||||
|
info.Fonts = append(info.Fonts, FontInfo{
|
||||||
|
ID: font.ID,
|
||||||
|
FontName: font.FontName,
|
||||||
|
FamilyName: font.FamilyName,
|
||||||
|
Charset: font.Charset,
|
||||||
|
FontFile: font.FontFile,
|
||||||
|
Embedded: font.FontFile != "",
|
||||||
|
Status: "pending",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.FontCount = len(info.Fonts)
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info 获取完整文档信息
|
||||||
// 返回: DocumentInfo 文档信息
|
// 返回: DocumentInfo 文档信息
|
||||||
func (s *Session) Info() DocumentInfo {
|
func (s *Session) Info() DocumentInfo {
|
||||||
info := DocumentInfo{
|
info := DocumentInfo{
|
||||||
@@ -339,14 +405,20 @@ func (s *Session) renderPageImage(page *ofdgo.PageContent, dpi float64) (image.I
|
|||||||
// signatureInfos 获取签名验证信息
|
// signatureInfos 获取签名验证信息
|
||||||
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
||||||
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
||||||
|
if s.signaturesRead {
|
||||||
|
return s.signatures, s.signatureError
|
||||||
|
}
|
||||||
|
s.signaturesRead = true
|
||||||
reports, err := s.Reader.VerifySignatures()
|
reports, err := s.Reader.VerifySignatures()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.signatureError = err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
infos := make([]SignatureInfo, 0, len(reports))
|
infos := make([]SignatureInfo, 0, len(reports))
|
||||||
for _, report := range reports {
|
for _, report := range reports {
|
||||||
infos = append(infos, signatureInfo(report))
|
infos = append(infos, signatureInfo(report))
|
||||||
}
|
}
|
||||||
|
s.signatures = infos
|
||||||
return infos, nil
|
return infos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +536,15 @@ func (s *Session) ExportPDF() ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := s.Renderer.RenderToMultiPagePDF(&buf); err != nil {
|
pages := make([]*ofdgo.PageContent, len(s.doc.Pages.Page))
|
||||||
|
for i := range pages {
|
||||||
|
_, page, err := s.pageContent(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages[i] = page
|
||||||
|
}
|
||||||
|
if err := s.Renderer.RenderPagesToPDF(pages, &buf); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
|||||||
fontDirCandidates: make(map[string][]fontFileCandidate),
|
fontDirCandidates: make(map[string][]fontFileCandidate),
|
||||||
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
|
textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue),
|
||||||
templatePageCache: make(map[string]*PageContent),
|
templatePageCache: make(map[string]*PageContent),
|
||||||
|
imageCache: make(map[string]image.Image),
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(r)
|
opt(r)
|
||||||
|
|||||||
@@ -248,6 +248,37 @@ func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
|||||||
return &content, nil
|
return &content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageArea 读取页面区域,不解析页面图元
|
||||||
|
// 入参: page 页面对象
|
||||||
|
// 返回: PageArea 页面区域, error 错误信息
|
||||||
|
func (r *Reader) PageArea(page Page) (PageArea, error) {
|
||||||
|
f, err := r.openFile(r.ResPath(page.BaseLoc))
|
||||||
|
if err != nil {
|
||||||
|
return PageArea{}, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
d := xml.NewDecoder(f)
|
||||||
|
for {
|
||||||
|
token, err := d.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
return PageArea{}, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return PageArea{}, err
|
||||||
|
}
|
||||||
|
if start, ok := token.(xml.StartElement); ok {
|
||||||
|
switch start.Name.Local {
|
||||||
|
case "Area":
|
||||||
|
var area PageArea
|
||||||
|
err := d.DecodeElement(&area, &start)
|
||||||
|
return area, err
|
||||||
|
case "Content":
|
||||||
|
return PageArea{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ResPath 获取资源的完整路径
|
// ResPath 获取资源的完整路径
|
||||||
// 入参: resLink 资源链接
|
// 入参: resLink 资源链接
|
||||||
// 返回: string 完整路径
|
// 返回: string 完整路径
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -39,6 +40,7 @@ type Renderer struct {
|
|||||||
fontDirCandidates map[string][]fontFileCandidate
|
fontDirCandidates map[string][]fontFileCandidate
|
||||||
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
|
textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue
|
||||||
templatePageCache map[string]*PageContent
|
templatePageCache map[string]*PageContent
|
||||||
|
imageCache map[string]image.Image
|
||||||
fontDirs []string
|
fontDirs []string
|
||||||
fontFS []fs.FS
|
fontFS []fs.FS
|
||||||
decodeImages bool
|
decodeImages bool
|
||||||
|
|||||||
+26
-7
@@ -188,7 +188,32 @@ func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix) imag
|
|||||||
// 入参: resPath 图片资源路径
|
// 入参: resPath 图片资源路径
|
||||||
// 返回: image.Image 图片对象, error 错误信息
|
// 返回: image.Image 图片对象, error 错误信息
|
||||||
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
||||||
rc, err := r.Reader.openFile(r.Reader.ResPath(resPath))
|
resPath = cleanPackagePath(r.Reader.ResPath(resPath))
|
||||||
|
img, ok := r.imageCache[resPath]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
img, err = r.readImageResource(resPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if r.imageCache == nil {
|
||||||
|
r.imageCache = make(map[string]image.Image)
|
||||||
|
}
|
||||||
|
r.imageCache[resPath] = img
|
||||||
|
}
|
||||||
|
if r.decodeImages {
|
||||||
|
if source, ok := img.(*canvasimage.Image); ok {
|
||||||
|
return source.Image()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readImageResource 读取图片资源并保留可直接嵌入的原始编码
|
||||||
|
// 入参: resPath 图片资源路径
|
||||||
|
// 返回: image.Image 图片对象, error 错误信息
|
||||||
|
func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
|
||||||
|
rc, err := r.Reader.openFile(resPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -200,9 +225,6 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if r.decodeImages {
|
|
||||||
return img.Image()
|
|
||||||
}
|
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
if isPNGData(header) {
|
if isPNGData(header) {
|
||||||
@@ -210,9 +232,6 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if r.decodeImages {
|
|
||||||
return img.Image()
|
|
||||||
}
|
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
|
|||||||
+13
-5
@@ -95,12 +95,20 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
currentCTM := NewMatrix(cgu.CTM)
|
|
||||||
if parentCTM != nil {
|
|
||||||
currentCTM = parentCTM.Multiply(currentCTM)
|
|
||||||
}
|
|
||||||
box, _ := ParseBox(cgu.Boundary)
|
box, _ := ParseBox(cgu.Boundary)
|
||||||
clipPath := intersectClipPath(parentClip, r.buildClipPath(cgu.Clips, pageH, box.X, box.Y, currentCTM))
|
boundaryCTM := TranslationMatrix(box.X, box.Y)
|
||||||
|
if parentCTM != nil {
|
||||||
|
boundaryCTM = parentCTM.Multiply(boundaryCTM)
|
||||||
|
}
|
||||||
|
currentCTM := boundaryCTM.Multiply(NewMatrix(cgu.CTM))
|
||||||
|
clips, clipCTM := cgu.Clips, currentCTM
|
||||||
|
if clips != nil && clips.TransFlag != nil && !*clips.TransFlag {
|
||||||
|
// 不参与对象CTM的裁剪仍位于对象边界及父级坐标系中
|
||||||
|
clipCopy := *clips
|
||||||
|
clipCopy.TransFlag = nil
|
||||||
|
clips, clipCTM = &clipCopy, boundaryCTM
|
||||||
|
}
|
||||||
|
clipPath := intersectClipPath(parentClip, r.buildClipPath(clips, pageH, 0, 0, clipCTM))
|
||||||
if cgu.ResourceID != "" {
|
if cgu.ResourceID != "" {
|
||||||
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
|
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
|
||||||
refCopy := *ref
|
refCopy := *ref
|
||||||
|
|||||||
+44
-25
@@ -67,26 +67,12 @@ func replacePDFProducer(data []byte) []byte {
|
|||||||
// 入参: page 页面内容, writer 输出流
|
// 入参: page 页面内容, writer 输出流
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
|
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
|
||||||
c, err := r.renderPage(page)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
box, err := r.GetPageBox(page)
|
box, err := r.GetPageBox(page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pages := []pdfPage{{Content: page, Box: box}}
|
pages := []pdfPage{{Content: page, Box: box}}
|
||||||
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
return r.renderPDFPages(pages, writer)
|
||||||
var buf bytes.Buffer
|
|
||||||
p := pdf.New(&buf, c.W, c.H, nil)
|
|
||||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
|
||||||
navigation.apply(p, 0)
|
|
||||||
c.RenderTo(p)
|
|
||||||
if err := p.Close(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderToEPS 渲染为EPS
|
// RenderToEPS 渲染为EPS
|
||||||
@@ -123,24 +109,57 @@ func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
|
|||||||
}
|
}
|
||||||
pages[i] = pdfPage{Content: page, Box: box}
|
pages[i] = pdfPage{Content: page, Box: box}
|
||||||
}
|
}
|
||||||
navigation := newPDFNavigation(r, doc, pages)
|
return r.renderPDFPages(pages, writer)
|
||||||
var buf bytes.Buffer
|
}
|
||||||
p := pdf.New(&buf, pages[0].Box.W, pages[0].Box.H, nil)
|
|
||||||
|
// RenderPagesToPDF 按指定顺序导出已解析的页面,复用调用方的页面缓存
|
||||||
|
// 入参: contents 页面内容列表, writer 输出流
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (r *Renderer) RenderPagesToPDF(contents []*PageContent, writer io.Writer) error {
|
||||||
|
if len(contents) == 0 {
|
||||||
|
return fmt.Errorf("no pages found")
|
||||||
|
}
|
||||||
|
pages := make([]pdfPage, len(contents))
|
||||||
|
for i, page := range contents {
|
||||||
|
box, err := r.GetPageBox(page)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read page %d area: %w", i+1, err)
|
||||||
|
}
|
||||||
|
pages[i] = pdfPage{Content: page, Box: box}
|
||||||
|
}
|
||||||
|
return r.renderPDFPages(pages, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderPDFPages 渲染页面并复用调用方的字节缓冲
|
||||||
|
// 入参: pages 页面列表, writer 输出流
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (r *Renderer) renderPDFPages(pages []pdfPage, writer io.Writer) error {
|
||||||
|
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
||||||
|
buf, direct := writer.(*bytes.Buffer)
|
||||||
|
if !direct {
|
||||||
|
buf = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
start := buf.Len()
|
||||||
|
p := pdf.New(buf, pages[0].Box.W, pages[0].Box.H, nil)
|
||||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
||||||
for i, page := range pages {
|
for i, page := range pages {
|
||||||
c, err := r.renderPage(page.Content)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
|
||||||
}
|
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
p.NewPage(c.W, c.H)
|
p.NewPage(page.Box.W, page.Box.H)
|
||||||
}
|
}
|
||||||
navigation.apply(p, i)
|
navigation.apply(p, i)
|
||||||
c.RenderTo(p)
|
if err := r.renderPageToContext(canvas.NewContext(p), page.Content, true); err != nil {
|
||||||
|
buf.Truncate(start)
|
||||||
|
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := p.Close(); err != nil {
|
if err := p.Close(); err != nil {
|
||||||
|
buf.Truncate(start)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
data := replacePDFProducer(buf.Bytes()[start:])
|
||||||
|
if direct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := writer.Write(data)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
+59
-2
@@ -29,6 +29,11 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if obj.Visible != nil && !*obj.Visible {
|
if obj.Visible != nil && !*obj.Visible {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
shouldFill := obj.Fill == nil || *obj.Fill
|
||||||
|
shouldStroke := obj.Stroke != nil && *obj.Stroke
|
||||||
|
if !shouldFill && !shouldStroke {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
bx, by := 0.0, 0.0
|
bx, by := 0.0, 0.0
|
||||||
if obj.Boundary != "" {
|
if obj.Boundary != "" {
|
||||||
@@ -84,6 +89,22 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if fillPaint == nil {
|
if fillPaint == nil {
|
||||||
fillPaint = fillColor
|
fillPaint = fillColor
|
||||||
}
|
}
|
||||||
|
strokeStyle := newPathStyle(nil, defaultStroke, 0, obj.Alpha)
|
||||||
|
if shouldStroke {
|
||||||
|
if dp != nil {
|
||||||
|
strokeStyle.applyDrawParam(dp, bx, by, pageH, obj.Alpha)
|
||||||
|
}
|
||||||
|
if obj.StrokeColor != nil {
|
||||||
|
strokeStyle.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
||||||
|
}
|
||||||
|
if obj.LineWidth > 0 {
|
||||||
|
strokeStyle.lineWidth = obj.LineWidth
|
||||||
|
}
|
||||||
|
if strokeStyle.strokePaint == nil {
|
||||||
|
strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||||
|
}
|
||||||
|
strokeStyle.scale(ctm)
|
||||||
|
}
|
||||||
fontStyle := canvas.FontRegular
|
fontStyle := canvas.FontRegular
|
||||||
weight := obj.Weight
|
weight := obj.Weight
|
||||||
if weight == 0 && dp != nil && dp.Weight > 0 {
|
if weight == 0 && dp != nil && dp.Weight > 0 {
|
||||||
@@ -136,8 +157,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||||
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
||||||
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil
|
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || clipPath != nil || shouldStroke
|
||||||
cx, cy := 0.0, 0.0
|
cx, cy := 0.0, 0.0
|
||||||
|
previousWidth := 0.0
|
||||||
if len(xs) > 0 {
|
if len(xs) > 0 {
|
||||||
cx = xs[0]
|
cx = xs[0]
|
||||||
}
|
}
|
||||||
@@ -160,9 +182,10 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if dx, ok := textDelta(dxs, i-1); ok {
|
if dx, ok := textDelta(dxs, i-1); ok {
|
||||||
cx += dx
|
cx += dx
|
||||||
} else if len(dys) == 0 {
|
} else if len(dys) == 0 {
|
||||||
cx += glyphWidth * hScale
|
cx += previousWidth * hScale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
previousWidth = glyphWidth
|
||||||
if i < len(ys) {
|
if i < len(ys) {
|
||||||
cy = ys[i]
|
cy = ys[i]
|
||||||
} else if i > 0 {
|
} else if i > 0 {
|
||||||
@@ -185,6 +208,40 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
||||||
}
|
}
|
||||||
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
|
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
|
||||||
|
if shouldStroke {
|
||||||
|
scaleX := hScale
|
||||||
|
if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit {
|
||||||
|
scaleX = advanceLimit / glyphWidth
|
||||||
|
}
|
||||||
|
transform := canvas.Identity.Translate(canvasX, canvasY)
|
||||||
|
if useTextMatrix {
|
||||||
|
transform = transform.Mul(textMatrix(ctm))
|
||||||
|
}
|
||||||
|
path := glyphPath.Copy().Transform(transform.Scale(scaleX, 1))
|
||||||
|
if shouldFill && glyphFillPaint != nil {
|
||||||
|
ctx.SetFill(glyphFillPaint)
|
||||||
|
ctx.SetStrokeColor(canvas.Transparent)
|
||||||
|
ctx.DrawPath(0, 0, applyClipPath(path.Copy(), clipPath))
|
||||||
|
}
|
||||||
|
if len(strokeStyle.dashPattern) > 0 {
|
||||||
|
path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...)
|
||||||
|
}
|
||||||
|
path = path.Stroke(strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, canvas.Tolerance)
|
||||||
|
ctx.SetFill(strokeStyle.strokePaint)
|
||||||
|
ctx.SetStrokeColor(canvas.Transparent)
|
||||||
|
ctx.DrawPath(0, 0, applyClipPath(path, clipPath))
|
||||||
|
if hasUnderline {
|
||||||
|
underline := &canvas.Path{}
|
||||||
|
underline.MoveTo(0, -sizeMM*0.1)
|
||||||
|
underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1)
|
||||||
|
underline = underline.Stroke(sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance)
|
||||||
|
if shouldFill {
|
||||||
|
ctx.SetFillColor(fillColor)
|
||||||
|
}
|
||||||
|
ctx.DrawPath(0, 0, applyClipPath(underline.Transform(transform), clipPath))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if glyphFillPaint != nil {
|
if glyphFillPaint != nil {
|
||||||
ctx.SetFill(glyphFillPaint)
|
ctx.SetFill(glyphFillPaint)
|
||||||
if clipPath != nil {
|
if clipPath != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user