mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
This commit is contained in:
1 parent
a4ae065d43
commit
651e3cf5b5
4 files changed
+135
-8
No files matched your search
+90
-8
@@ -57,6 +57,7 @@ const state = {
|
|||||||
pageCache: new Map(),
|
pageCache: new Map(),
|
||||||
svgFonts: new Map(),
|
svgFonts: new Map(),
|
||||||
selectedPages: new Set(),
|
selectedPages: new Set(),
|
||||||
|
documentSelection: null,
|
||||||
pageInFlight: new Map(),
|
pageInFlight: new Map(),
|
||||||
pageRenderQueue: [],
|
pageRenderQueue: [],
|
||||||
pageRenderRunning: false,
|
pageRenderRunning: false,
|
||||||
@@ -207,11 +208,7 @@ el.panButton.addEventListener("click", togglePan);
|
|||||||
el.continuousButton.addEventListener("click", toggleContinuous);
|
el.continuousButton.addEventListener("click", toggleContinuous);
|
||||||
el.annotationButton.addEventListener("click", toggleAnnotations);
|
el.annotationButton.addEventListener("click", toggleAnnotations);
|
||||||
document.addEventListener("copy", copySelection);
|
document.addEventListener("copy", copySelection);
|
||||||
document.addEventListener("selectionchange", () => {
|
document.addEventListener("selectionchange", syncSelection);
|
||||||
for (const index of state.selectedPages) {
|
|
||||||
unmountPage(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||||
el.exportButton.addEventListener("click", openExportPanel);
|
el.exportButton.addEventListener("click", openExportPanel);
|
||||||
@@ -289,10 +286,22 @@ updateSidebarState();
|
|||||||
boot();
|
boot();
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.defaultPrevented || event.isComposing || event.altKey || document.body.hasAttribute("aria-busy") || el.exportPanel.open) {
|
if (event.defaultPrevented || event.isComposing || event.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
const target = event.target;
|
||||||
|
if ((event.ctrlKey || event.metaKey) && !event.shiftKey && key.toLowerCase() === "a" && state.doc
|
||||||
|
&& el.viewerPanel.contains(target) && !target.closest("input, textarea, select, [contenteditable]")) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!document.body.hasAttribute("aria-busy") && !el.exportPanel.open) {
|
||||||
|
selectDocumentText();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (document.body.hasAttribute("aria-busy") || el.exportPanel.open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
if (!event.shiftKey && key.toLowerCase() === "o") {
|
if (!event.shiftKey && key.toLowerCase() === "o") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -306,7 +315,13 @@ function handleKeyDown(event) {
|
|||||||
if (!state.doc) {
|
if (!state.doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = event.target;
|
if (key === "Escape" && !event.shiftKey && state.documentSelection && el.viewerPanel.contains(target)
|
||||||
|
&& !target.closest("input, textarea, select, [contenteditable]")) {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getSelection().removeAllRanges();
|
||||||
|
syncSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (key === "F3") {
|
if (key === "F3") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (state.searchMatches.length) {
|
if (state.searchMatches.length) {
|
||||||
@@ -1684,6 +1699,7 @@ function resetPageFlow() {
|
|||||||
state.svgFonts.clear();
|
state.svgFonts.clear();
|
||||||
state.pageCache.clear();
|
state.pageCache.clear();
|
||||||
state.selectedPages.clear();
|
state.selectedPages.clear();
|
||||||
|
state.documentSelection = null;
|
||||||
state.visiblePages.clear();
|
state.visiblePages.clear();
|
||||||
for (const task of state.pageInFlight.values()) {
|
for (const task of state.pageInFlight.values()) {
|
||||||
task.resolve(null);
|
task.resolve(null);
|
||||||
@@ -1740,7 +1756,8 @@ function flowPageObserver() {
|
|||||||
function unmountPage(index) {
|
function unmountPage(index) {
|
||||||
const shell = pageShell(index);
|
const shell = pageShell(index);
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
if (selection.rangeCount && selection.getRangeAt(0).intersectsNode(shell)) {
|
const range = selection.rangeCount ? selection.getRangeAt(0) : null;
|
||||||
|
if (range && (!state.documentSelection || !isDocumentSelection(range)) && range.intersectsNode(shell)) {
|
||||||
state.selectedPages.add(index);
|
state.selectedPages.add(index);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1943,6 +1960,62 @@ function createTextLayer(text) {
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSelection() {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
if (state.documentSelection && (!selection.rangeCount || !isDocumentSelection(selection.getRangeAt(0)))) {
|
||||||
|
state.documentSelection = null;
|
||||||
|
if (state.doc && !document.body.hasAttribute("aria-busy")) {
|
||||||
|
setStatus(pageStatus(state.pageIndex, state.doc.pageCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const index of state.selectedPages) {
|
||||||
|
unmountPage(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentSelection(range) {
|
||||||
|
return range.startContainer === el.svgHost && range.startOffset === 0
|
||||||
|
&& range.endContainer === el.svgHost && range.endOffset === el.svgHost.childNodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDocumentText() {
|
||||||
|
const selection = document.getSelection();
|
||||||
|
if (state.documentSelection && selection.rangeCount && isDocumentSelection(selection.getRangeAt(0))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const openSeq = state.openSeq;
|
||||||
|
const current = { text: null };
|
||||||
|
const active = () => state.documentSelection === current && openSeq === state.openSeq
|
||||||
|
&& selection.rangeCount > 0 && isDocumentSelection(selection.getRangeAt(0));
|
||||||
|
state.documentSelection = current;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(el.svgHost);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
const pages = [];
|
||||||
|
const count = state.doc.pageCount;
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < count; index += 1) {
|
||||||
|
setStatus(`正在全选 ${index + 1} / ${count} 页`);
|
||||||
|
const text = await callWASM("ofdgoPageTextString", index);
|
||||||
|
if (!active()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
pages.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.text = pages.join("\n");
|
||||||
|
setStatus(current.text ? "文字已全选" : "暂无文字");
|
||||||
|
} catch (err) {
|
||||||
|
if (active()) {
|
||||||
|
state.documentSelection = null;
|
||||||
|
selection.removeAllRanges();
|
||||||
|
showError(err, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function copySelection(event) {
|
function copySelection(event) {
|
||||||
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
if (event.target.closest?.("input, textarea, [contenteditable]")) {
|
||||||
return;
|
return;
|
||||||
@@ -1952,6 +2025,15 @@ function copySelection(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
|
if (state.documentSelection && isDocumentSelection(range)) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (state.documentSelection.text === null) {
|
||||||
|
setStatus("正在全选文字");
|
||||||
|
} else {
|
||||||
|
event.clipboardData.setData("text/plain", state.documentSelection.text);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!el.svgHost.contains(range.startContainer) || !el.svgHost.contains(range.endContainer)) {
|
if (!el.svgHost.contains(range.startContainer) || !el.svgHost.contains(range.endContainer)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoRenderPage", renderPage)
|
registerCallback("ofdgoRenderPage", renderPage)
|
||||||
registerCallback("ofdgoSVGFontData", svgFontData)
|
registerCallback("ofdgoSVGFontData", svgFontData)
|
||||||
registerCallback("ofdgoSearchPage", searchPage)
|
registerCallback("ofdgoSearchPage", searchPage)
|
||||||
|
registerCallback("ofdgoPageTextString", pageTextString)
|
||||||
registerCallback("ofdgoExportFormats", exportFormats)
|
registerCallback("ofdgoExportFormats", exportFormats)
|
||||||
registerCallback("ofdgoParsePageRange", parsePageRange)
|
registerCallback("ofdgoParsePageRange", parsePageRange)
|
||||||
registerExportCallback("ofdgoExportPage", exportPage)
|
registerExportCallback("ofdgoExportPage", exportPage)
|
||||||
@@ -283,6 +284,16 @@ func renderPage(args []js.Value) (any, error) {
|
|||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pageTextString 提取OFD页面原文
|
||||||
|
// 入参: args 浏览器参数
|
||||||
|
// 返回: any 页面原文, error 错误信息
|
||||||
|
func pageTextString(args []js.Value) (any, error) {
|
||||||
|
if currentSession == nil {
|
||||||
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
|
}
|
||||||
|
return currentSession.PageTextString(args[0].Int())
|
||||||
|
}
|
||||||
|
|
||||||
// searchPage 搜索OFD页面文字
|
// searchPage 搜索OFD页面文字
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 搜索结果, error 错误信息
|
// 返回: any 搜索结果, error 错误信息
|
||||||
|
|||||||
@@ -289,6 +289,24 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
|
|||||||
return text, nil
|
return text, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageTextString 获取页面原文,不为全文复制保留额外的页面和文字索引
|
||||||
|
// 入参: index 页面索引
|
||||||
|
// 返回: string 页面原文, error 错误信息
|
||||||
|
func (s *Session) PageTextString(index int) (string, error) {
|
||||||
|
text := s.textCache[index]
|
||||||
|
if text == nil {
|
||||||
|
page, err := s.Reader.PageContentByIndex(index)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
text, err = s.Renderer.PageText(page)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// SearchPage 搜索指定页面,复用当前会话的文字索引
|
// SearchPage 搜索指定页面,复用当前会话的文字索引
|
||||||
// 入参: index 页面索引, query 搜索文字
|
// 入参: index 页面索引, query 搜索文字
|
||||||
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
|
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ func (r *Renderer) PageText(page *PageContent) (*PageText, error) {
|
|||||||
return renderer.pageText, nil
|
return renderer.pageText, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String 按文本对象顺序合并原文,以换行分隔非空对象
|
||||||
|
// 返回: string 页面原文
|
||||||
|
func (p *PageText) String() string {
|
||||||
|
var text strings.Builder
|
||||||
|
for _, run := range p.Runs {
|
||||||
|
if run.Text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if text.Len() > 0 {
|
||||||
|
text.WriteByte('\n')
|
||||||
|
}
|
||||||
|
text.WriteString(run.Text)
|
||||||
|
}
|
||||||
|
return text.String()
|
||||||
|
}
|
||||||
|
|
||||||
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
|
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
|
||||||
// 入参: query 搜索文字
|
// 入参: query 搜索文字
|
||||||
// 返回: []TextMatch 匹配结果及上下文
|
// 返回: []TextMatch 匹配结果及上下文
|
||||||
|
|||||||
Reference in new issue
Block a user