自定义 Excalidraw 脚本 - OCR 自动提取图片文字

自定义 Excalidraw 脚本 - OCR 自动提取图片文字

Cite

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--

对 Excalidraw 的图片进行 OCR,并保留文本信息在图片中,可以编辑修改、重新 OCR 和进行批量识别。

脚本文件

脚本链接

可以用 Excalidraw Scripts 代码块安装:

https://raw.githubusercontent.com/PandaNocturne/ExcalidrawScripts/master/PandaScripts/TextExtractor.md

配置文件

配置文件 paddleocr.zip,解压到到 .obsidian 文件夹下即可采用脚本的默认配置:

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--配置文件

脚本思路

  • 对图片添加弹窗,如果不存在 OCR 文本则进行 OCR,然后保留数据文本在 Yaml 中 (参考 Excalidraw 自带的识别保留识别数据结构),弹窗可以添加。
  • 对图片进行 OCR,可以保存数据为到指定 Cache 文件夹为 Json 数据 (参考 Text Extractor 的数据存储结构)

在控制台的使用 Text Extractor 的 API 案例:


function getTextExtractor() {
	return app.plugins.plugins['text-extractor'].api;
}
// Example usage
let file={
    path:'Y-图形文件存储/ZoteroImages/JPQENLZK.png',
};

const text = await getTextExtractor().extractText(file)

console.log(text)

OcrText 保存方式

默认只会保留图片编辑后的数据文件到画板的 Yaml 区域:

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--OcrText 保存方式

如果图片不存在,该 Yaml 对应的值会在下一次运行脚本的时候删除,减少冗余数据

TextCache 本地数据缓存

对同一个图片的编辑结果在其他画板中不会保留,因此在设置选项中有 TextCache 选项可以保存同一张图片的编辑结果,使你在不同画板中对同一个图片的编辑数据保留,如果更改,存储数据也会更改,数据保存格式为 Json 文件。

📌注意:开启后,需要配置数据保存的文件夹,相对库的相对路径

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--TextCache 本地数据缓存

选中多个图片可以进行批量识别但不会弹窗

Ocr 识别模式

识别模式 1:插件 Text Extractor

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--识别模式 1:插件 Text Extractor

需要配置 Text Extractor 插件安装即可使用,注意要在 Text Extractor 插件设置里面开启中文识别模式,这样中文的准确率会稍微好一点。

Text Extractor 设置参数如下:

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--识别模式 1:插件 Text Extractor

识别模式 2:本地模型 Paddleocr

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--识别模式 2:本地模型 Paddleocr

采用的是百度的飞桨 Paddleocr 模型,中文识别率会好很多。

如果不会配置请使用 Text Extractor 的识别方法

前者脚本只需要配合 Text Extractor 插件联网使用,后者需要手动安装配置文件可以离线使用且与 Text Extractor 插件兼容。

再开始配置之前我先展示下下面代码的大概存储结构:

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--识别模式 2:本地模型 Paddleocr

下载 PaddleOCR-json 模型

将压缩包解压为文件夹,改名为“PaddleOCR-json”,然后就不需要动了

采用 Python 调用的 PPOCR_api.py 文件

因为我 js 是半吊子,采用对的是 Python 调用的形式,代码能跑就行。

下载 PPOCR_api.py 或者复制下面代码为 PPOCR_api.py 文件中

# 调用 PaddleOCR-json.exe 的 Python Api
# 项目主页:
# https://github.com/hiroi-sora/PaddleOCR-json

import os
import socket # 套接字
import subprocess  # 进程,管道
from json import loads as jsonLoads, dumps as jsonDumps
from sys import platform as sysPlatform  # popen静默模式
from base64 import b64encode # base64 编码


class PPOCR_pipe:
    """调用OCR(管道模式)"""

    def __init__(self, exePath: str, argument: dict = None):
        """初始化识别器(管道模式)。\n
        `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
        `argument`: 启动参数,字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json
        """
        cwd = os.path.abspath(os.path.join(exePath, os.pardir))  # 获取exe父文件夹
        # 处理启动参数
        if not argument == None:
            for key, value in argument.items():
                if isinstance(value, str):  # 字符串类型的值加双引号
                    exePath += f' --{key}="{value}"'
                else:
                    exePath += f" --{key}={value}"
        # 设置子进程启用静默模式,不显示控制台窗口
        startupinfo = None
        if "win32" in str(sysPlatform).lower():
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
            startupinfo.wShowWindow = subprocess.SW_HIDE
        self.ret = subprocess.Popen(  # 打开管道
            exePath, cwd=cwd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,  # 丢弃stderr的内容
            startupinfo=startupinfo  # 开启静默模式
        )
        # 启动子进程
        while True:
            if not self.ret.poll() == None:  # 子进程已退出,初始化失败
                raise Exception(f"OCR init fail.")
            initStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
            if "OCR init completed." in initStr:  # 初始化成功
                break

    def runDict(self, writeDict: dict):
        """传入指令字典,发送给引擎进程。\n
        `writeDict`: 指令字典。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        # 检查子进程
        if not self.ret.poll() == None:
            return {"code": 901, "data": f"子进程已崩溃。"}
        # 输入信息
        writeStr = jsonDumps(writeDict, ensure_ascii=True, indent=None)+"\n"
        try:
            self.ret.stdin.write(writeStr.encode("utf-8"))
            self.ret.stdin.flush()
        except Exception as e:
            return {"code": 902, "data": f"向识别器进程传入指令失败,疑似子进程已崩溃。{e}"}
        # 获取返回值
        try:
            getStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
        except Exception as e:
            return {"code": 903, "data": f"读取识别器进程输出值失败。异常信息:[{e}]"}
        try:
            return jsonLoads(getStr)
        except Exception as e:
            return {"code": 904, "data": f"识别器输出值反序列化JSON失败。异常信息:[{e}]。原始内容:[{getStr}]"}

    def run(self, imgPath: str):
        """对一张本地图片进行文字识别。\n
        `exePath`: 图片路径。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        writeDict = {"image_path": imgPath}
        return self.runDict(writeDict)

    def runClipboard(self):
        """立刻对剪贴板第一位的图片进行文字识别。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        return self.run("clipboard")

    def runBase64(self, imageBase64: str):
        """对一张编码为base64字符串的图片进行文字识别。\n
        `imageBase64`: 图片base64字符串。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        writeDict = {"image_base64": imageBase64}
        return self.runDict(writeDict)

    def runBytes(self, imageBytes):
        """对一张图片的字节流信息进行文字识别。\n
        `imageBytes`: 图片字节流。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        imageBase64 = b64encode(imageBytes).decode('utf-8')
        return self.runBase64(imageBase64)

    def exit(self):
        """关闭引擎子进程"""
        self.ret.kill()  # 关闭子进程

    @staticmethod
    def printResult(res: dict):
        """用于调试,格式化打印识别结果。\n
        `res`: OCR识别结果。"""

        # 识别成功
        if res["code"] == 100:
            index = 1
            for line in res["data"]:
                print(f"{index}-置信度:{round(line['score'], 2)},文本:{line['text']}")
                index+=1
        elif res["code"] == 100:
            print("图片中未识别出文字。")
        else:
            print(f"图片识别失败。错误码:{res['code']},错误信息:{res['data']}")

    def __del__(self):
        self.exit()

class PPOCR_socket(PPOCR_pipe):
    """调用OCR(套接字模式)"""

    def __init__(self, exePath: str, argument: dict = None):
        """初始化识别器(套接字模式)。\n
        `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
        `argument`: 启动参数,字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json
        """
        # 处理参数
        if not argument:
            argument = {}
        argument["port"] = 0 # 随机端口号
        argument["addr"] = "loopback" # 本地环回地址
        super().__init__(exePath, argument) # 父类构造函数
        # 再获取一行输出,检查是否成功启动服务器
        initStr = self.ret.stdout.readline().decode("utf-8", errors="ignore")
        if not self.ret.poll() == None:  # 子进程已退出,初始化失败
            raise Exception(f"Socket init fail.")
        if "Socket init completed. " in initStr:  # 初始化成功
            splits = initStr.split(":")
            self.ip = splits[0].split("Socket init completed. ")[1]
            self.port = int(splits[1])   # 提取端口号
            self.ret.stdout.close() # 关闭管道重定向,防止缓冲区填满导致堵塞
            print(f"套接字服务器初始化成功。{self.ip}:{self.port}")
            return
        # 异常
        self.exit()
        raise Exception(f"Socket init fail.")

    def runDict(self, writeDict: dict):
        """传入指令字典,发送给引擎进程。\n
        `writeDict`: 指令字典。\n
        `return`:  {"code": 识别码, "data": 内容列表或错误信息字符串}\n"""
        # 检查子进程
        if not self.ret.poll() == None:
            return {"code": 901, "data": f"子进程已崩溃。"}
        # 通信
        writeStr = jsonDumps(writeDict, ensure_ascii=True, indent=None)+"\n"
        try:
            # 创建TCP连接
            clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            clientSocket.connect((self.ip, self.port))
            # 发送数据
            clientSocket.sendall(writeStr.encode())
            # 接收数据
            resData = b''
            while True:
                chunk = clientSocket.recv(1024)
                if not chunk:
                    break
                resData += chunk
            getStr = resData.decode()
        except ConnectionRefusedError:
            return {"code":902, "data": "连接被拒绝"}
        except TimeoutError:
            return {"code":903, "data": "连接超时"}
        except Exception as e:
            return {"code":904, "data": f"网络错误:{e}"}
        finally:
            clientSocket.close()  # 关闭连接
        # 反序列输出信息
        try:
            return jsonLoads(getStr)
        except Exception as e:
            return {"code": 905, "data": f"识别器输出值反序列化JSON失败。异常信息:[{e}]。原始内容:[{getStr}]"}

def GetOcrApi(exePath: str, argument: dict = None, ipcMode: str = "pipe"):
    """获取识别器API对象。\n
    `exePath`: 识别器`PaddleOCR_json.exe`的路径。\n
    `argument`: 启动参数,字典`{"键":值}`。参数说明见 https://github.com/hiroi-sora/PaddleOCR-json\n
    `ipcMode`: 进程通信模式,可选值为套接字模式`socket` 或 管道模式`pipe`。用法上完全一致。
    """
    if ipcMode == "socket":
        return PPOCR_socket(exePath, argument)
    elif ipcMode == "pipe":
        return PPOCR_pipe(exePath, argument)
    else:
        raise Exception(f'ipcMode可选值为 套接字模式"socket" 或 管道模式"pipe" ,不允许{ipcMode}。')

配置 PaddleocrToJson.py 文件

复制下面代码为 PaddleocrToJson.py 文件

#coding:utf-8
from PPOCR_api import GetOcrApi
import json
import os
import sys

# 测试图片路径
TestImagePath = sys.argv[1]

# 初始化识别器对象,传入 PaddleOCR-json.exe 的路径。
ocr = GetOcrApi(f"{os.path.dirname(os.path.abspath(__file__))}/PaddleOCR-json/PaddleOCR-json.exe")

# 识别本地图片
res = ocr.run(TestImagePath)
print(f'{json.dumps(res, ensure_ascii=False)}')

3 个配置文件的结构形式

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--配置 PaddleocrToJson.py 文件

配置 TextExtractor 脚本

有 3 种模式

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--配置 TextExtractor 脚本

首次加载脚本

Tip

首次次运行脚本或者重启 ob 会在 Excalidraw 插件中加载该选项
自定义 Excalidraw 脚本 - OCR 自动提取图片文字--首次加载脚本

单个图片文本数据缓存功能同样也可以开启,只是在原基础上添加本地识别的模型而已

自定义 Excalidraw 脚本 - OCR 自动提取图片文字--首次加载脚本

TextExtractor 本地版脚本

await ea.addElementsToView();
const api = ea.getExcalidrawAPI();
const fs = require('fs');
const path = require('path');
const Activefile = app.workspace.getActiveFile();
const { exec } = require('child_process');
const fileName = app.workspace.getActiveFile().name;

let settings = ea.getScriptSettings();
//set default values on first run
if (!settings["ocrModel2"]) {
	settings = {
		"ocrModel2": {
			value: "Paddleocr",
			valueset: ["Paddleocr", "TextExtractor", "无"],
			description: "选择 OCR 模型,有本地的 Paddleocr(需要本地文件)、Obsidian 的 Text Extractor 插件 API",
		},
		"PaddleocrPath": {
			value: ".obsidian/paddlleocr/PaddleocrToJson.py",
			description: "选择 paddlleocr 文件夹路径下的 PaddleocrToJson.py 文件"
		},
		"TextCache": {
			value: false,
			description: "是否存储文本数据到 JSON 文件中,如果图片已经编辑过后,会保留编辑后的数据,防止二次编辑"
		},
		"TextCachePath": {
			value: "",
			description: "如果开启 TextCache,请选择图片 OCR 的文本数据存储位置(相对于库的文件夹路径)"
		}
	};
	ea.setScriptSettings(settings);
}

// 获取库的基本路径
const basePath = (app.vault.adapter).getBasePath();
const textCachePath = `${basePath}/${settings["TextCachePath"].value}`;

if (!fs.existsSync(textCachePath)) {
	fs.mkdirSync(textCachePath, { recursive: true });
	console.log('配置路径已创建');
} else {
	console.log('配置路径已存在');
}

// !添加 ocrText 属性
await app.fileManager.processFrontMatter(Activefile, fm => {
	if (typeof fm[`ocrText`] !== 'object') fm[`ocrText`] = {};
});

console.log("写入 Yaml");

// ! text 类型
const selectedTextElements = ea.getViewSelectedElements().filter(el => el.type === "text");

if (selectedTextElements.length === 1) {
	ea.copyViewElementsToEAforEditing(selectedTextElements);
	const el = ea.getElements()[0];
	let exText = el.rawText;
	const { insertType, ocrTextEdit } = await openEditPrompt(exText);

	if (insertType == "copyText") {
		copyToClipboard(ocrTextEdit);
		new Notice(`已复制文本`, 1000);
	} else if (insertType) {
		copyToClipboard(ocrTextEdit);
		new Notice(`完成修改`, 500);
	}

	el.originalText = el.rawText = el.text = ocrTextEdit;
	// 文本全部居左,居中
	el.textAlign = "left";
	el.textVerticalAlign = "middle";
	ea.refreshTextElementSize(el.id);
	await ea.addElementsToView(false, false);
	if (el.containerId) {
		const containers = ea.getViewElements().filter(e => e.id === el.containerId);
		api.updateContainerSize(containers);
		ea.selectElementsInView(containers);
	}
	return;

}

// ! frame 类型
const selectedFrameElements = ea.getViewSelectedElements().filter(el => el.type === "frame");
if (selectedFrameElements.length === 1) {
	const el = selectedFrameElements[0];
	let exText = el.name;
	const { insertType, ocrTextEdit } = await openEditPrompt(exText, 1);
	const frameLink = `[[${fileName}#^frame=${el.id}|${ocrTextEdit}]]`;

	if (insertType == "copyText") {
		copyToClipboard(frameLink);
		new Notice(`已复制Frame${ocrTextEdit}的链接`, 2000);
	} else if (insertType) {
		copyToClipboard(frameLink);
		new Notice(`完成修改并复制链接`, 500);
		el.name = ocrTextEdit;
	} else {
		el.name = ocrTextEdit;
	}
	ea.copyViewElementsToEAforEditing(selectedFrameElements);
	await ea.addElementsToView(false, true);
} else if ((selectedFrameElements.length > 1)) {
	let frameLinks = [];
	for (el of selectedFrameElements) {
		const frameLink = `[[${fileName}#^frame=${el.id}|${el.name}]]`;
		frameLinks.push(frameLink);
	}
	copyToClipboard(frameLinks.join("\n"));
	ea.copyViewElementsToEAforEditing(selectedFrameElements);
	await ea.addElementsToView(false, true);
	new Notice(`已复制${frameLinks}链接`, 2000);
}

if (selectedFrameElements.length >= 1) {
	// ! 给aliaes添加所有Frame的名称
	const allFrameElements = ea.getViewElements().filter(el => el.type === "frame");
	await app.fileManager.processFrontMatter(Activefile, fm => {
		fm.aliases = [];
		for (el of allFrameElements) {
			fm.aliases.push(el.name);
		}
	})
	await ea.addElementsToView(); 
}

// ! 图片 OCR 或文本编辑
const els = ea.getViewSelectedElements().filter(el => el.type === "text" || el.type === "image" || el.type === "embeddable");
if (els.length >= 1) {
	// 是否为批处理
	const nums = els.filter(el => el.type == "image" || el.type === "text").length;
	let batchRecognition = false;

	// 多文本则进行批处理
	if (nums > 1) {
		new Notice(`检测到${nums}张图片\n进行批量识别`, 500);
		batchRecognition = true;
	}

	// 图片计数
	let n = 0;

	// 汇集所有文本集合
	let allText = [];
	// 获取库所有文件列表
	const files = app.vault.getFiles();

	for (let el of els) {
		if (el.type == "image") {
			let data = {
				filePath: "",
				fileId: "",
				ocrText: "",
			};
			const currentPath = ea.plugin.filesMaster.get(el.fileId).path;
			const file = app.vault.getAbstractFileByPath(currentPath);


			// 获取图片路径
			const imagePath = app.vault.adapter.getFullPath(file.path);
			console.log(`获取图片路径:${imagePath}`);

			const jsonPath = path.join(textCachePath, `${el.fileId}.json`);

			// 判断是否进行存储Json数据
			let jsonData = {};
			if (settings["TextCache"].value) {
				jsonData = readJsonData(jsonPath, data);
				console.log(jsonData.valueOf());
			} else {
				jsonData = {};
			}

			// 初始化ocr文本
			let ocrText = "";
			let ocrText_yaml = "";
			n++;

			await app.fileManager.processFrontMatter(Activefile, fm => {
				ocrText_yaml = fm[`ocrText`]?.[`${el.fileId}`];
			});

			if (ocrText_yaml) {
				ocrText = JSON.parse(ocrText_yaml);
			} else if (jsonData.ocrText) {
				new Notice(`图片已存在OCR文本`, 500);
				ocrText = jsonData.ocrText;
			} else if (settings["ocrModel2"].value == "Paddleocr") {
				new Notice(`图片OCR中......`);
				// 其次执行Paddleocr,如果报错则会保留ocrText的值
				const scriptPath = `${basePath}/${settings["PaddleocrPath"].value}`;
				console.log(scriptPath);
				await runPythonScript(scriptPath, imagePath)
					.then(output => {
						// 在这里处理Python脚本的输出
						console.log(output);
						let paddlleocrJson = JSON.parse(output);
						let paddlleocrText = paddlleocrJson.data.map(item => item.text);
						ocrText = paddlleocrText.join("\n");
						new Notice(`第${n}张片已完成OCR`, 500);

					})
					.catch(error => {
						new Notice(`Paddleocr识别失败,采用TextExtractor`);
						console.error(error);
					});

			} else if (settings["ocrModel2"].value == "TextExtractor") {
				new Notice(`图片OCR中......`);
				const text = await getTextExtractor().extractText(file);
				new Notice(`第${n}张片已完成OCR`, 500);
				ocrText = processText(text);
			}

			if (!batchRecognition) {
				const { insertType, ocrTextEdit } = await openEditPrompt(ocrText);
				// 不管复制还是修改,都会保存
				ocrText = ocrTextEdit;
				if (insertType == "copyText") {
					copyToClipboard(ocrTextEdit);
					new Notice(`已复制:图片文本`, 1000);
				} else if (insertType) {
					new Notice(`完成修改`, 500);
				}
			}

			// 更新数据源
			data.filePath = file.path;
			data.fileId = el.fileId;
			data.ocrText = ocrText;

			// 保存信息到Yaml区
			await app.fileManager.processFrontMatter(Activefile, fm => {
				fm[`ocrText`][`${el.fileId}`] = JSON.stringify(ocrText);
			});
			console.log("写入Yaml");

			if (settings["TextCache"].value) {
				// 保存数据到Json文件中
				fs.writeFileSync(jsonPath, JSON.stringify(data));
			}
			// 收集提取的信息
			allText.push(ocrText);

		} else if (el.type == "text") {
			let exText = el.rawText;
			console.log(exText);
			allText.push(exText);
		} else if (el.type == "embeddable" && el.link.endsWith("]]")) {
			let filePaths = getFilePath(files, el);
			// 读取文件内容
			let markdownText = getMarkdownText(filePaths);
			console.log(markdownText);
			allText.push(markdownText);

			copyToClipboard(markdownText);
			new Notice(`复制文本`, 3000);
		}
		await ea.addElementsToView(false, true);
	}
	await ea.addElementsToView(false, true);

	if (batchRecognition) {
		// 如果批量识别则直接进行复制文本
		const output = allText.join("\n");
		console.log(output);
		new Notice(`✅已完成批量OCR`, 3000);
		copyToClipboard(output);
		new Notice(`📋复制所有文本到剪切板`, 3000);
	}

}

// ! 如果图片不存在则清理 yaml 对应的 id
await app.fileManager.processFrontMatter(Activefile, fm => {
	allels = ea.getViewElements();
	Object.keys(fm.ocrText).forEach(key => {
		console.log(key);
		if ((!allels.some(el => `${el.fileId}` === key)) || fm[key] === "\"\"") {
			delete fm.ocrText[key];
		}
	});
});

// 调用 Text Extractor 的 API
function getTextExtractor() {
	return app.plugins.plugins['text-extractor'].api;
}

// 格式化文本
function processText(text) {
	// 替换特殊空格为普通空格
	text = text.replace(/[\ue5d2\u00a0\u2007\u202F\u3000\u314F\u316D\ue5cf]/g, ' ');
	// 将全角字符转换为半角字符
	text = text.replace(/[\uFF01-\uFF5E]/g, function (match) { return String.fromCharCode(match.charCodeAt(0) - 65248); });
	// 替换英文之间的多个空格为一个空格
	text = text.replace(/([a-zA-Z])([\u4e00-\u9fa5])/g, '$1 $2');

	// 删除中文之间的空格
	text = text.replace(/([0-9\.\u4e00-\u9fa5])\s+([0-9\.\u4e00-\u9fa5])/g, '$1$2');
	text = text.replace(/([0-9\.\u4e00-\u9fa5])\s+([0-9\.\u4e00-\u9fa5])/g, '$1$2');
	text = text.replace(/([\u4e00-\u9fa5])\s+/g, '$1');
	text = text.replace(/\s+([\u4e00-\u9fa5])/g, '$1');

	// 在中英文之间添加空格
	text = text.replace(/([\u4e00-\u9fa5])([a-zA-Z])/g, '$1 $2');
	text = text.replace(/([a-zA-Z])([\u4e00-\u9fa5])/g, '$1 $2');

	return text;

}

// 打开文本编辑器
async function openEditPrompt(ocrText, n = 10) {
	// 打开编辑窗口
	let insertType = "";
	let ocrTextEdit = await utils.inputPrompt(
		"编辑文本",
		"可以自行修改文字保存在图片的属性中,输入一个空格会重新识别,注意清空并不会清除数据",
		ocrText,
		[

			{
				caption: "修改",
				action: () => {
					insertType = "insertImage";
					return;
				}
			},
			{
				caption: "复制",
				action: () => {
					insertType = "copyText";
					return;
				}
			},
		],
		n,
		true
	);

	if (!ocrTextEdit) {
		ocrTextEdit = ocrText;
	} else if (ocrTextEdit == " ") {
		ocrTextEdit = "";
	}

	return { insertType, ocrTextEdit };

}

// 复制内容到剪切板
function copyToClipboard(extrTexts) {
	const txtArea = document.createElement('textarea');
	txtArea.value = extrTexts;
	document.body.appendChild(txtArea);
	txtArea.select();
	if (document.execCommand('copy')) {
		console.log('copy to clipboard.');
	} else {
		console.log('fail to copy.');
	}
	document.body.removeChild(txtArea);
}

// 读取 Json 数据文件转为对象
function readJsonData(jsonPath, data) {
	if (!fs.existsSync(jsonPath)) {
		console.log('文件不存在');
		fs.writeFileSync(jsonPath, JSON.stringify(data));
	} else {
		console.log('文件已存在');
	}
	const existingDataString = fs.readFileSync(jsonPath, 'utf8');
	let jsonData = JSON.parse(existingDataString);
	return jsonData;
}

// 获取文件路径下的 md 中的文本(排除 Yaml)
function getMarkdownText(filePath) {
	// 获取文件的完整路径
	const fileFullPath = app.vault.adapter.getFullPath(filePath);

	// 读取文件内容
	const fileContent = fs.readFileSync(fileFullPath, 'utf8');

	// 排除首行YAML区域
	const markdownText = fileContent.replace(/---[\s\S]*?---/, '').replace(/\n\n/, "\n");

	return markdownText;

}

// 由文件列表和 el 元素获取文件路径(相对路径)
function getFilePath(files, el) {
	let files2 = files.filter(f => path.basename(f.path).replace(".md", "").endsWith(el.link.replace(/\[\[/, "").replace(/\|.\*]]/, "").replace(/\]\]/, "").replace(".md", "")));
	let filePath = files2.map((f) => f.path)[0];
	console.log(filePath);
	return filePath;
}

// 运行本地 Python 文件
function runPythonScript(scriptPath, args) {
	return new Promise((resolve, reject) => {
		const command = `python "${scriptPath}" "${args}"`;
		exec(command, (error, stdout, stderr) => {
			if (error) {
				console.error(`执行命令时发生错误: ${error.message}`);
				reject(error);
			}
			if (stderr) {
				console.error(`命令执行返回错误: ${stderr}`);
				reject(stderr);
			}
			resolve(stdout.trim());
		});
	});
}

常见 bug

本地版运行不了或者出现 Bug,不想折腾请转插件版,另外你想继续深究这里可以提供几个常见 bug 以及解决方法:

  1. 本地版运行不了:
    • 是否安装 Python 编辑器,有时候 Anaconda 装了但没有配置 Python 系统环境同样运行不了,
      • 方法 1:安装官方 Python
      • 方法 2:给 Anaconda 配置系统环境
        • 自定义 Excalidraw 脚本 - OCR 自动提取图片文字--常见 bug
  2. 识别出来的是乱码:
    • 这是你没给 Python 配置 utf-8 的系统环境
      • 自定义 Excalidraw 脚本 - OCR 自动提取图片文字--常见 bug
        • PYTHONIOENCODING
        • UTF8

已有的 bug 已经给你标注出来了,剩余的问题自己折腾,画板的这个功能其实用处不大,简单娱乐的脚本而已。

Log

因为我本人只使用本地模型的识别模式 2 的代码,所以修改部分全部更新在识别模式 2 中,识别模式 1 存在问题不会修复,识别模式 2 是兼容识别模式 1 的,只要不设置本地 Paddleocr 模式就行。

  • 2023-12-29:
    • 优化 OCR 识别文本的 Yaml 数据结构存储
    • 修复修改文本后不同步问题
  • 2024-03-02:
    • 添加 当编辑 Frame 名称后,会将名称添加到 Yaml 的 aliases 属性中,方便通过别名来定位 Excalidraw 内的标题

讨论

若阁下有独到的见解或新颖的想法,诚邀您在文章下方留言,与大家共同探讨。



反馈交流

其他渠道

版权声明