@bill_nee/dwg-reader 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +157 -0
- package/package.json +2 -2
- package/scripts/ocr_image.py +70 -0
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const MAX_DWG_BYTES = 512 * 1024 * 1024; // 512 MB 上限,超大图纸会先
|
|
|
8
8
|
const MAX_ENTITY_DETAIL = 2000;
|
|
9
9
|
const PYTHON_EXE = "D:\\dsh-home\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe";
|
|
10
10
|
const DXF_SCRIPT = fileURLToPath(new URL("../scripts/read_dxf.py", import.meta.url));
|
|
11
|
+
const OCR_SCRIPT = fileURLToPath(new URL("../scripts/ocr_image.py", import.meta.url));
|
|
11
12
|
const MAX_PDF_BYTES = 100 * 1024 * 1024; // 100 MB PDF 上限
|
|
12
13
|
|
|
13
14
|
let libreDwgPromise;
|
|
@@ -571,10 +572,166 @@ export function apply(ctx, config) {
|
|
|
571
572
|
const disposeDwg = ctx.tools.register(buildReadDwgTool(ctx));
|
|
572
573
|
const disposeDxf = ctx.tools.register(buildReadDxfTool(ctx));
|
|
573
574
|
const disposePdf = ctx.tools.register(buildReadPdfTool(ctx));
|
|
575
|
+
const disposeImage = ctx.tools.register(buildReadImageTool(ctx));
|
|
574
576
|
return () => {
|
|
575
577
|
disposeDwg();
|
|
576
578
|
disposeDxf();
|
|
577
579
|
disposePdf();
|
|
580
|
+
disposeImage();
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function buildReadImageTool(ctx) {
|
|
585
|
+
return {
|
|
586
|
+
name: "read_image",
|
|
587
|
+
description:
|
|
588
|
+
"读取图片/截屏文件(尺寸、格式、元数据 + Tesseract OCR 文字提取,无需安装图片查看软件)。" +
|
|
589
|
+
"支持 PNG/JPEG/WebP/BMP/TIFF;OCR 语言为简体中文+英文。",
|
|
590
|
+
parameters: {
|
|
591
|
+
type: "object",
|
|
592
|
+
additionalProperties: true,
|
|
593
|
+
properties: {
|
|
594
|
+
file_path: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description: "图片文件路径(绝对路径或相对当前工作目录)。",
|
|
597
|
+
},
|
|
598
|
+
ocr: {
|
|
599
|
+
type: "boolean",
|
|
600
|
+
description: "是否执行 OCR 文字提取(默认 true)。",
|
|
601
|
+
},
|
|
602
|
+
tessdata_prefix: {
|
|
603
|
+
type: "string",
|
|
604
|
+
description: "Tesseract tessdata 目录(可选;缺省使用系统默认)。",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
required: ["file_path"],
|
|
608
|
+
},
|
|
609
|
+
output: {
|
|
610
|
+
schema: {
|
|
611
|
+
type: "object",
|
|
612
|
+
additionalProperties: true,
|
|
613
|
+
properties: {
|
|
614
|
+
path: { type: "string", description: "解析后的文件路径。" },
|
|
615
|
+
fileName: { type: "string", description: "文件名。" },
|
|
616
|
+
fileSizeBytes: { type: "integer", description: "文件字节数。" },
|
|
617
|
+
format: { type: "string", description: "图片格式(如 PNG/JPEG)。" },
|
|
618
|
+
width: { type: "integer", description: "图片宽度(像素)。" },
|
|
619
|
+
height: { type: "integer", description: "图片高度(像素)。" },
|
|
620
|
+
mode: { type: "string", description: "颜色模式(如 RGB/RGBA)。" },
|
|
621
|
+
ocrText: { type: "string", description: "OCR 提取的文字(可能为空)。" },
|
|
622
|
+
ocrError: { type: "string", description: "OCR 错误信息;成功时为 null。" },
|
|
623
|
+
error: { type: "string", description: "错误信息;成功时为 null。" },
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
render: (args, value) => {
|
|
627
|
+
const lines = [];
|
|
628
|
+
lines.push(`<path>${value.path}</path>`);
|
|
629
|
+
lines.push(`<type>image</type>`);
|
|
630
|
+
if (value.error) {
|
|
631
|
+
lines.push(`<content>读取图片失败:${value.error}</content>`);
|
|
632
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
633
|
+
}
|
|
634
|
+
const sizeKb = (value.fileSizeBytes / 1024).toFixed(1);
|
|
635
|
+
lines.push(`<content>已解析图片:${value.fileName}(${sizeKb} KB)`);
|
|
636
|
+
lines.push(`格式:${value.format};尺寸:${value.width}×${value.height};模式:${value.mode}`);
|
|
637
|
+
if (value.ocrError) lines.push(`OCR 错误:${value.ocrError}`);
|
|
638
|
+
if (value.ocrText) {
|
|
639
|
+
const preview = value.ocrText.slice(0, 300);
|
|
640
|
+
lines.push(`OCR 文字(${value.ocrText.length} 字符):${preview}${value.ocrText.length > 300 ? "…" : ""}`);
|
|
641
|
+
} else if (!value.ocrError) {
|
|
642
|
+
lines.push("OCR 文字:无(未检测到可识别文字或 OCR 未启用)");
|
|
643
|
+
}
|
|
644
|
+
lines.push(`</content>`);
|
|
645
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
646
|
+
},
|
|
647
|
+
presentationMeta: () => null,
|
|
648
|
+
},
|
|
649
|
+
isConcurrencySafe: () => false,
|
|
650
|
+
presentCall: (args) => ({
|
|
651
|
+
card: "generic",
|
|
652
|
+
title: `读取图片 ${args.file_path || "(未指定路径)"}`,
|
|
653
|
+
kind: "read",
|
|
654
|
+
locations: [{ path: args.file_path || "" }],
|
|
655
|
+
}),
|
|
656
|
+
presentResult: (args, result) => {
|
|
657
|
+
if (result && result.isError) return undefined;
|
|
658
|
+
return {
|
|
659
|
+
card: "generic",
|
|
660
|
+
title: `图片解析完成:${args.file_path || ""}`,
|
|
661
|
+
kind: "read",
|
|
662
|
+
locations: [{ path: args.file_path || "" }],
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
async execute(args, exec) {
|
|
666
|
+
if (typeof args !== "object" || args === null) {
|
|
667
|
+
throw new Error('invalid arguments: expected an object with a "file_path" string');
|
|
668
|
+
}
|
|
669
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim()) {
|
|
670
|
+
throw new Error('invalid arguments: "file_path" must be a non-empty string');
|
|
671
|
+
}
|
|
672
|
+
if (args.ocr !== undefined && typeof args.ocr !== "boolean") {
|
|
673
|
+
throw new Error('invalid arguments: "ocr" must be a boolean');
|
|
674
|
+
}
|
|
675
|
+
if (args.tessdata_prefix !== undefined && typeof args.tessdata_prefix !== "string") {
|
|
676
|
+
throw new Error('invalid arguments: "tessdata_prefix" must be a string');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const resolveOptions = await sessionResolveOptions(ctx, exec);
|
|
680
|
+
const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
|
|
681
|
+
const info = await ctx.fs.stat(target, exec.signal);
|
|
682
|
+
const displayPath = ctx.fs.processPath(target);
|
|
683
|
+
if (!info) {
|
|
684
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
685
|
+
throw new Error(`cannot read "${displayPath}": not found`);
|
|
686
|
+
}
|
|
687
|
+
if (info.type !== "file") {
|
|
688
|
+
throw new Error(`cannot read "${displayPath}": not a regular file`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const pythonArgs = [
|
|
692
|
+
OCR_SCRIPT,
|
|
693
|
+
args.file_path.trim(),
|
|
694
|
+
"D:\\Tools\\Tesseract-OCR\\tesseract.exe",
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
const result = await new Promise((resolve, reject) => {
|
|
698
|
+
const proc = spawn(PYTHON_EXE, pythonArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
699
|
+
let stdout = "";
|
|
700
|
+
let stderr = "";
|
|
701
|
+
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
702
|
+
proc.stderr.on("data", (d) => (stderr += d.toString()));
|
|
703
|
+
proc.on("close", (code) => {
|
|
704
|
+
if (code !== 0) {
|
|
705
|
+
reject(new Error(`Python 进程退出码 ${code}: ${stderr || stdout}`));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
resolve(JSON.parse(stdout));
|
|
710
|
+
} catch (err) {
|
|
711
|
+
reject(new Error(`OCR 结果解析失败: ${err.message}; stdout=${stdout.slice(0, 200)}`));
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
proc.on("error", (err) => reject(new Error(`无法启动 Python 进程: ${err.message}`)));
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
if (result.error) {
|
|
718
|
+
throw new Error(`cannot read "${displayPath}": ${result.error}`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
722
|
+
|
|
723
|
+
return toJson({
|
|
724
|
+
path: displayPath,
|
|
725
|
+
fileName: displayPath.split(/[\\/]/).pop(),
|
|
726
|
+
fileSizeBytes: result.fileSizeBytes ?? info.size !== undefined ? info.size : 0,
|
|
727
|
+
format: result.format ?? "",
|
|
728
|
+
width: result.width ?? null,
|
|
729
|
+
height: result.height ?? null,
|
|
730
|
+
mode: result.mode ?? null,
|
|
731
|
+
ocrText: result.ocrText ?? "",
|
|
732
|
+
ocrError: result.ocrError ?? null,
|
|
733
|
+
});
|
|
734
|
+
},
|
|
578
735
|
};
|
|
579
736
|
}
|
|
580
737
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill_nee/dwg-reader",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "直接读取 AutoCAD DWG/DXF
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "直接读取 AutoCAD DWG/DXF、PDF 与图片/截屏的 DSH 插件(DWG 基于 libredwg-web 0.8.2 WASM,DXF 基于 Python ezdxf 桥接,PDF 基于 pdf-parse,图片基于 Pillow+Tesseract OCR,GPL-3.0)。",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"author": "MEB 工程造价项目助手 <meb@example.com>",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""对图片进行 OCR 文字提取(供 DSH dwg-reader 插件调用,直接调用 tesseract 命令行)。"""
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
IMAGE_PATH = sys.argv[1] if len(sys.argv) > 1 else None
|
|
10
|
+
TESSERACT_CMD = sys.argv[2] if len(sys.argv) > 2 else r"D:\Tools\Tesseract-OCR\tesseract.exe"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
if not IMAGE_PATH or not os.path.exists(IMAGE_PATH):
|
|
15
|
+
print(json.dumps({"error": f"文件不存在: {IMAGE_PATH}"}))
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from PIL import Image
|
|
20
|
+
except ImportError as exc:
|
|
21
|
+
print(json.dumps({"error": f"Pillow 未安装: {exc}"}))
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
image = Image.open(IMAGE_PATH)
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
print(json.dumps({"error": f"无法打开图片: {exc}"}))
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
width, height = image.size
|
|
31
|
+
fmt = image.format
|
|
32
|
+
mode = image.mode
|
|
33
|
+
|
|
34
|
+
# 调用 tesseract 命令行做 OCR
|
|
35
|
+
ocr_text = ""
|
|
36
|
+
ocr_error = None
|
|
37
|
+
try:
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
39
|
+
out_base = os.path.join(tmpdir, "ocr_out")
|
|
40
|
+
cmd = [TESSERACT_CMD, IMAGE_PATH, out_base, "-l", "chi_sim+eng"]
|
|
41
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
42
|
+
if proc.returncode != 0:
|
|
43
|
+
ocr_error = proc.stderr.strip() or f"tesseract 退出码 {proc.returncode}"
|
|
44
|
+
else:
|
|
45
|
+
out_txt = out_base + ".txt"
|
|
46
|
+
if os.path.exists(out_txt):
|
|
47
|
+
with open(out_txt, "r", encoding="utf-8", errors="replace") as f:
|
|
48
|
+
ocr_text = f.read().strip()
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
ocr_error = str(exc)
|
|
51
|
+
|
|
52
|
+
result = {
|
|
53
|
+
"path": os.path.abspath(IMAGE_PATH),
|
|
54
|
+
"fileName": os.path.basename(IMAGE_PATH),
|
|
55
|
+
"fileSizeBytes": os.path.getsize(IMAGE_PATH),
|
|
56
|
+
"format": fmt,
|
|
57
|
+
"width": width,
|
|
58
|
+
"height": height,
|
|
59
|
+
"mode": mode,
|
|
60
|
+
"ocrText": ocr_text,
|
|
61
|
+
"ocrError": ocr_error,
|
|
62
|
+
}
|
|
63
|
+
# Windows 控制台默认 GBK,改为 UTF-8 输出到 stdout.buffer 避免编码错误
|
|
64
|
+
sys.stdout.buffer.write(json.dumps(result, ensure_ascii=False).encode("utf-8"))
|
|
65
|
+
sys.stdout.buffer.write(b"\n")
|
|
66
|
+
sys.stdout.buffer.flush()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|