@bill_nee/dwg-reader 1.1.1 → 1.3.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 +365 -2
- package/package.json +15 -3
- package/scripts/read_dxf.py +104 -0
package/lib/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { PDFParse } from "pdf-parse";
|
|
2
4
|
import { LibreDwg, Dwg_File_Type, DwgCodePage } from "../vendor/libredwg-web/lib/index.js";
|
|
3
5
|
|
|
4
6
|
const WASM_DIR = fileURLToPath(new URL("../vendor/libredwg-web/wasm", import.meta.url));
|
|
5
7
|
const MAX_DWG_BYTES = 512 * 1024 * 1024; // 512 MB 上限,超大图纸会先于 WASM 内存耗尽给出明确报错
|
|
6
8
|
const MAX_ENTITY_DETAIL = 2000;
|
|
9
|
+
const PYTHON_EXE = "D:\\dsh-home\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe";
|
|
10
|
+
const DXF_SCRIPT = fileURLToPath(new URL("../scripts/read_dxf.py", import.meta.url));
|
|
11
|
+
const MAX_PDF_BYTES = 100 * 1024 * 1024; // 100 MB PDF 上限
|
|
7
12
|
|
|
8
13
|
let libreDwgPromise;
|
|
9
14
|
|
|
@@ -371,9 +376,367 @@ function buildReadDwgTool(ctx) {
|
|
|
371
376
|
};
|
|
372
377
|
}
|
|
373
378
|
|
|
379
|
+
function buildReadDxfTool(ctx) {
|
|
380
|
+
return {
|
|
381
|
+
name: "read_dxf",
|
|
382
|
+
description:
|
|
383
|
+
"读取 DXF 图纸文件(基于 Python ezdxf 桥接,无需安装 AutoCAD)。" +
|
|
384
|
+
"返回 DXF 版本、图层表、块数量、实体统计(按类型计数)与可选实体明细。",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
additionalProperties: true,
|
|
388
|
+
properties: {
|
|
389
|
+
file_path: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "DXF 文件路径(绝对路径或相对当前工作目录)。",
|
|
392
|
+
},
|
|
393
|
+
entities: {
|
|
394
|
+
type: "boolean",
|
|
395
|
+
description: "是否返回实体明细列表(默认 false,只返回统计)。",
|
|
396
|
+
},
|
|
397
|
+
entity_limit: {
|
|
398
|
+
type: "integer",
|
|
399
|
+
description: `实体明细最多返回条数(默认 200,上限 ${MAX_ENTITY_DETAIL})。`,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ["file_path"],
|
|
403
|
+
},
|
|
404
|
+
output: {
|
|
405
|
+
schema: {
|
|
406
|
+
type: "object",
|
|
407
|
+
additionalProperties: true,
|
|
408
|
+
properties: {
|
|
409
|
+
path: { type: "string", description: "解析后的文件路径。" },
|
|
410
|
+
fileName: { type: "string", description: "文件名。" },
|
|
411
|
+
fileSizeBytes: { type: "integer", description: "文件字节数。" },
|
|
412
|
+
format: { type: "string", description: "文件格式(固定为 dxf)。" },
|
|
413
|
+
dxfVersion: { type: "string", description: "DXF 版本代号(如 R2010)。" },
|
|
414
|
+
header: { type: "object", additionalProperties: true, description: "图头关键变量。" },
|
|
415
|
+
layerCount: { type: "integer", description: "图层数量。" },
|
|
416
|
+
layers: {
|
|
417
|
+
type: "array",
|
|
418
|
+
items: { type: "object", additionalProperties: true },
|
|
419
|
+
description: "图层表(名称/颜色/线型/开关状态)。",
|
|
420
|
+
},
|
|
421
|
+
blockCount: { type: "integer", description: "块定义数量。" },
|
|
422
|
+
entityCount: { type: "integer", description: "实体总数。" },
|
|
423
|
+
entityTypeCounts: {
|
|
424
|
+
type: "object",
|
|
425
|
+
additionalProperties: true,
|
|
426
|
+
description: "按实体类型统计数量(如 { LINE: 120, TEXT: 8 })。",
|
|
427
|
+
},
|
|
428
|
+
entities: {
|
|
429
|
+
type: "array",
|
|
430
|
+
items: { type: "object", additionalProperties: true },
|
|
431
|
+
description: "实体明细(仅当 entities=true 时非空)。",
|
|
432
|
+
},
|
|
433
|
+
entitiesTruncated: { type: "boolean", description: "实体明细是否被截断。" },
|
|
434
|
+
error: { type: "string", description: "错误信息;成功时为 null。" },
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
render: (args, value) => {
|
|
438
|
+
const lines = [];
|
|
439
|
+
lines.push(`<path>${value.path}</path>`);
|
|
440
|
+
lines.push(`<type>dxf</type>`);
|
|
441
|
+
if (value.error) {
|
|
442
|
+
lines.push(`<content>读取 DXF 失败:${value.error}</content>`);
|
|
443
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
444
|
+
}
|
|
445
|
+
const sizeMb = (value.fileSizeBytes / 1024 / 1024).toFixed(2);
|
|
446
|
+
lines.push(`<content>已解析 DXF:${value.fileName}(${sizeMb} MB)`);
|
|
447
|
+
lines.push(`版本:${value.dxfVersion}`);
|
|
448
|
+
lines.push(`图层:${value.layerCount} 个${value.layers.length ? " — " + value.layers.map((l) => l.name).join(", ") : ""}`);
|
|
449
|
+
lines.push(`块定义:${value.blockCount} 个;实体:${value.entityCount} 个`);
|
|
450
|
+
const typeLines = Object.entries(value.entityTypeCounts)
|
|
451
|
+
.sort((a, b) => b[1] - a[1])
|
|
452
|
+
.slice(0, 20)
|
|
453
|
+
.map(([t, n]) => `${t} ${n}`)
|
|
454
|
+
.join(",");
|
|
455
|
+
if (typeLines) lines.push(`实体构成:${typeLines}${Object.keys(value.entityTypeCounts).length > 20 ? ",…" : ""}`);
|
|
456
|
+
if (value.header && value.header.$ACADVER) lines.push(`图版:${value.header.$ACADVER}`);
|
|
457
|
+
if (value.entities && value.entities.length) {
|
|
458
|
+
lines.push(`实体明细(${value.entities.length}${value.entitiesTruncated ? ",已截断" : ""}):`);
|
|
459
|
+
const shown = value.entities.slice(0, 50);
|
|
460
|
+
for (const [i, e] of shown.entries()) {
|
|
461
|
+
lines.push(
|
|
462
|
+
` ${i + 1}. ${e.type} handle=${e.handle} layer=${e.layer}` +
|
|
463
|
+
(e.colorIndex != null ? ` color=${e.colorIndex}` : "") +
|
|
464
|
+
(e.lineType != null ? ` linetype=${e.lineType}` : "")
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
if (value.entities.length > shown.length) lines.push(` …(其余 ${value.entities.length - shown.length} 条未显示)`);
|
|
468
|
+
}
|
|
469
|
+
lines.push(`</content>`);
|
|
470
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
471
|
+
},
|
|
472
|
+
presentationMeta: () => null,
|
|
473
|
+
},
|
|
474
|
+
isConcurrencySafe: () => false,
|
|
475
|
+
presentCall: (args) => ({
|
|
476
|
+
card: "generic",
|
|
477
|
+
title: `读取 DXF ${args.file_path || "(未指定路径)"}`,
|
|
478
|
+
kind: "read",
|
|
479
|
+
locations: [{ path: args.file_path || "" }],
|
|
480
|
+
}),
|
|
481
|
+
presentResult: (args, result) => {
|
|
482
|
+
if (result && result.isError) return undefined;
|
|
483
|
+
return {
|
|
484
|
+
card: "generic",
|
|
485
|
+
title: `DXF 解析完成:${args.file_path || ""}`,
|
|
486
|
+
kind: "read",
|
|
487
|
+
locations: [{ path: args.file_path || "" }],
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
async execute(args, exec) {
|
|
491
|
+
if (typeof args !== "object" || args === null) {
|
|
492
|
+
throw new Error('invalid arguments: expected an object with a "file_path" string');
|
|
493
|
+
}
|
|
494
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim()) {
|
|
495
|
+
throw new Error('invalid arguments: "file_path" must be a non-empty string');
|
|
496
|
+
}
|
|
497
|
+
if (args.entities !== undefined && typeof args.entities !== "boolean") {
|
|
498
|
+
throw new Error('invalid arguments: "entities" must be a boolean');
|
|
499
|
+
}
|
|
500
|
+
if (args.entity_limit !== undefined && (typeof args.entity_limit !== "number" || !Number.isInteger(args.entity_limit))) {
|
|
501
|
+
throw new Error('invalid arguments: "entity_limit" must be an integer');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const resolveOptions = await sessionResolveOptions(ctx, exec);
|
|
505
|
+
const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
|
|
506
|
+
const info = await ctx.fs.stat(target, exec.signal);
|
|
507
|
+
const displayPath = ctx.fs.processPath(target);
|
|
508
|
+
if (!info) {
|
|
509
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
510
|
+
throw new Error(`cannot read "${displayPath}": not found`);
|
|
511
|
+
}
|
|
512
|
+
if (info.type !== "file") {
|
|
513
|
+
throw new Error(`cannot read "${displayPath}": not a regular file`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const pythonArgs = [
|
|
517
|
+
DXF_SCRIPT,
|
|
518
|
+
args.file_path.trim(),
|
|
519
|
+
String(args.entity_limit || 200),
|
|
520
|
+
String(args.entities === true),
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
const result = await new Promise((resolve, reject) => {
|
|
524
|
+
const proc = spawn(PYTHON_EXE, pythonArgs, { stdio: ["pipe", "pipe", "pipe"] });
|
|
525
|
+
let stdout = "";
|
|
526
|
+
let stderr = "";
|
|
527
|
+
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
528
|
+
proc.stderr.on("data", (d) => (stderr += d.toString()));
|
|
529
|
+
proc.on("close", (code) => {
|
|
530
|
+
if (code !== 0) {
|
|
531
|
+
reject(new Error(`Python 进程退出码 ${code}: ${stderr || stdout}`));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
resolve(JSON.parse(stdout));
|
|
536
|
+
} catch (err) {
|
|
537
|
+
reject(new Error(`DXF 结果解析失败: ${err.message}; stdout=${stdout.slice(0, 200)}`));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
proc.on("error", (err) => reject(new Error(`无法启动 Python 进程: ${err.message}`)));
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
if (result.error) {
|
|
544
|
+
throw new Error(`cannot read "${displayPath}": ${result.error}`);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
548
|
+
|
|
549
|
+
return toJson({
|
|
550
|
+
path: displayPath,
|
|
551
|
+
fileName: displayPath.split(/[\\/]/).pop(),
|
|
552
|
+
fileSizeBytes: result.fileSizeBytes ?? info.size !== undefined ? info.size : 0,
|
|
553
|
+
format: result.format ?? "dxf",
|
|
554
|
+
dxfVersion: result.dxfVersion ?? "",
|
|
555
|
+
header: result.header ?? {},
|
|
556
|
+
layerCount: result.layerCount ?? 0,
|
|
557
|
+
layers: result.layers ?? [],
|
|
558
|
+
blockCount: result.blockCount ?? 0,
|
|
559
|
+
entityCount: result.entityCount ?? 0,
|
|
560
|
+
entityTypeCounts: result.entityTypeCounts ?? {},
|
|
561
|
+
entities: result.entities ?? [],
|
|
562
|
+
entitiesTruncated: !!result.entitiesTruncated,
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
374
568
|
export const inject = ["tools", "fs", "workingDirectory"];
|
|
375
569
|
|
|
376
570
|
export function apply(ctx, config) {
|
|
377
|
-
const
|
|
378
|
-
|
|
571
|
+
const disposeDwg = ctx.tools.register(buildReadDwgTool(ctx));
|
|
572
|
+
const disposeDxf = ctx.tools.register(buildReadDxfTool(ctx));
|
|
573
|
+
const disposePdf = ctx.tools.register(buildReadPdfTool(ctx));
|
|
574
|
+
return () => {
|
|
575
|
+
disposeDwg();
|
|
576
|
+
disposeDxf();
|
|
577
|
+
disposePdf();
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function buildReadPdfTool(ctx) {
|
|
582
|
+
return {
|
|
583
|
+
name: "read_pdf",
|
|
584
|
+
description:
|
|
585
|
+
"读取 PDF 文件(基于 pdf-parse,无需安装 Adobe Acrobat)。" +
|
|
586
|
+
"返回 PDF 页数、元数据(标题/作者/创建日期等)与可选全文文本。",
|
|
587
|
+
parameters: {
|
|
588
|
+
type: "object",
|
|
589
|
+
additionalProperties: true,
|
|
590
|
+
properties: {
|
|
591
|
+
file_path: {
|
|
592
|
+
type: "string",
|
|
593
|
+
description: "PDF 文件路径(绝对路径或相对当前工作目录)。",
|
|
594
|
+
},
|
|
595
|
+
extract_text: {
|
|
596
|
+
type: "boolean",
|
|
597
|
+
description: "是否提取全文文本(默认 true)。",
|
|
598
|
+
},
|
|
599
|
+
max_text_chars: {
|
|
600
|
+
type: "integer",
|
|
601
|
+
description: "提取文本的最大字符数(默认 50000,0 表示不限制)。",
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
required: ["file_path"],
|
|
605
|
+
},
|
|
606
|
+
output: {
|
|
607
|
+
schema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
additionalProperties: true,
|
|
610
|
+
properties: {
|
|
611
|
+
path: { type: "string", description: "解析后的文件路径。" },
|
|
612
|
+
fileName: { type: "string", description: "文件名。" },
|
|
613
|
+
fileSizeBytes: { type: "integer", description: "文件字节数。" },
|
|
614
|
+
format: { type: "string", description: "文件格式(固定为 pdf)。" },
|
|
615
|
+
pageCount: { type: "integer", description: "PDF 页数。" },
|
|
616
|
+
metadata: { type: "object", additionalProperties: true, description: "PDF 元数据(标题、作者、创建日期等)。" },
|
|
617
|
+
text: { type: "string", description: "提取的全文文本(可能被截断)。" },
|
|
618
|
+
textLength: { type: "integer", description: "提取文本的字符数。" },
|
|
619
|
+
textTruncated: { type: "boolean", description: "文本是否被截断。" },
|
|
620
|
+
error: { type: "string", description: "错误信息;成功时为 null。" },
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
render: (args, value) => {
|
|
624
|
+
const lines = [];
|
|
625
|
+
lines.push(`<path>${value.path}</path>`);
|
|
626
|
+
lines.push(`<type>pdf</type>`);
|
|
627
|
+
if (value.error) {
|
|
628
|
+
lines.push(`<content>读取 PDF 失败:${value.error}</content>`);
|
|
629
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
630
|
+
}
|
|
631
|
+
const sizeMb = (value.fileSizeBytes / 1024 / 1024).toFixed(2);
|
|
632
|
+
lines.push(`<content>已解析 PDF:${value.fileName}(${sizeMb} MB)`);
|
|
633
|
+
lines.push(`页数:${value.pageCount}`);
|
|
634
|
+
if (value.metadata && Object.keys(value.metadata).length) {
|
|
635
|
+
const metaLines = Object.entries(value.metadata)
|
|
636
|
+
.filter(([, v]) => v != null && String(v).trim())
|
|
637
|
+
.map(([k, v]) => ` ${k}: ${v}`)
|
|
638
|
+
.join("\n");
|
|
639
|
+
if (metaLines) lines.push(`元数据:\n${metaLines}`);
|
|
640
|
+
}
|
|
641
|
+
lines.push(`文本长度:${value.textLength} 字符${value.textTruncated ? "(已截断)" : ""}`);
|
|
642
|
+
if (value.text) {
|
|
643
|
+
const preview = value.text.slice(0, 300);
|
|
644
|
+
lines.push(`文本预览:${preview}${value.text.length > 300 ? "…" : ""}`);
|
|
645
|
+
}
|
|
646
|
+
lines.push(`</content>`);
|
|
647
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
648
|
+
},
|
|
649
|
+
presentationMeta: () => null,
|
|
650
|
+
},
|
|
651
|
+
isConcurrencySafe: () => false,
|
|
652
|
+
presentCall: (args) => ({
|
|
653
|
+
card: "generic",
|
|
654
|
+
title: `读取 PDF ${args.file_path || "(未指定路径)"}`,
|
|
655
|
+
kind: "read",
|
|
656
|
+
locations: [{ path: args.file_path || "" }],
|
|
657
|
+
}),
|
|
658
|
+
presentResult: (args, result) => {
|
|
659
|
+
if (result && result.isError) return undefined;
|
|
660
|
+
return {
|
|
661
|
+
card: "generic",
|
|
662
|
+
title: `PDF 解析完成:${args.file_path || ""}`,
|
|
663
|
+
kind: "read",
|
|
664
|
+
locations: [{ path: args.file_path || "" }],
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
async execute(args, exec) {
|
|
668
|
+
if (typeof args !== "object" || args === null) {
|
|
669
|
+
throw new Error('invalid arguments: expected an object with a "file_path" string');
|
|
670
|
+
}
|
|
671
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim()) {
|
|
672
|
+
throw new Error('invalid arguments: "file_path" must be a non-empty string');
|
|
673
|
+
}
|
|
674
|
+
if (args.extract_text !== undefined && typeof args.extract_text !== "boolean") {
|
|
675
|
+
throw new Error('invalid arguments: "extract_text" must be a boolean');
|
|
676
|
+
}
|
|
677
|
+
if (args.max_text_chars !== undefined && (typeof args.max_text_chars !== "number" || !Number.isInteger(args.max_text_chars))) {
|
|
678
|
+
throw new Error('invalid arguments: "max_text_chars" must be an integer');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const resolveOptions = await sessionResolveOptions(ctx, exec);
|
|
682
|
+
const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
|
|
683
|
+
const info = await ctx.fs.stat(target, exec.signal);
|
|
684
|
+
const displayPath = ctx.fs.processPath(target);
|
|
685
|
+
if (!info) {
|
|
686
|
+
ctx.emit("fs/observed", target, { kind: "absent" }, exec);
|
|
687
|
+
throw new Error(`cannot read "${displayPath}": not found`);
|
|
688
|
+
}
|
|
689
|
+
if (info.type !== "file") {
|
|
690
|
+
throw new Error(`cannot read "${displayPath}": not a regular file`);
|
|
691
|
+
}
|
|
692
|
+
if (info.size !== undefined && info.size > MAX_PDF_BYTES) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`cannot read "${displayPath}": file is ${(info.size / 1024 / 1024).toFixed(1)} MB, ` +
|
|
695
|
+
`超过 ${MAX_PDF_BYTES / 1024 / 1024} MB 上限`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
const bytes = await ctx.fs.readBytes(target, exec.signal, MAX_PDF_BYTES);
|
|
699
|
+
ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
|
|
700
|
+
|
|
701
|
+
let parser;
|
|
702
|
+
try {
|
|
703
|
+
parser = new PDFParse({ data: bytes });
|
|
704
|
+
} catch (error) {
|
|
705
|
+
throw new Error(`cannot parse "${displayPath}": 初始化 PDF 解析器失败: ${error.message}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let text = "";
|
|
709
|
+
let textTruncated = false;
|
|
710
|
+
let pageCount = 0;
|
|
711
|
+
let metadata = {};
|
|
712
|
+
try {
|
|
713
|
+
const infoResult = await parser.getInfo();
|
|
714
|
+
pageCount = infoResult.info?.Pages != null ? Number(infoResult.info.Pages) : 0;
|
|
715
|
+
metadata = infoResult.metadata ?? {};
|
|
716
|
+
if (args.extract_text !== false) {
|
|
717
|
+
const textResult = await parser.getText();
|
|
718
|
+
text = textResult?.text ?? "";
|
|
719
|
+
const maxChars = args.max_text_chars ?? 50000;
|
|
720
|
+
if (maxChars > 0 && text.length > maxChars) {
|
|
721
|
+
text = text.slice(0, maxChars);
|
|
722
|
+
textTruncated = true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
throw new Error(`cannot parse "${displayPath}": PDF 内容提取失败: ${error.message}`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return toJson({
|
|
730
|
+
path: displayPath,
|
|
731
|
+
fileName: displayPath.split(/[\\/]/).pop(),
|
|
732
|
+
fileSizeBytes: info.size !== undefined ? info.size : bytes.byteLength,
|
|
733
|
+
format: "pdf",
|
|
734
|
+
pageCount,
|
|
735
|
+
metadata,
|
|
736
|
+
text,
|
|
737
|
+
textLength: text.length,
|
|
738
|
+
textTruncated,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
};
|
|
379
742
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill_nee/dwg-reader",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "直接读取 AutoCAD DWG
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "直接读取 AutoCAD DWG/DXF 与 PDF 文件的 DSH 插件(DWG 基于 libredwg-web 0.8.2 WASM,DXF 基于 Python ezdxf 桥接,PDF 基于 pdf-parse,GPL-3.0)。",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"author": "MEB 工程造价项目助手 <meb@example.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/NIDEXIANG/dwg-reader-plugin-20261029.git"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"dwg",
|
|
13
|
+
"dwg-reader",
|
|
14
|
+
"autocad",
|
|
15
|
+
"cad",
|
|
16
|
+
"dsh-plugin",
|
|
17
|
+
"libredwg",
|
|
18
|
+
"wasm"
|
|
19
|
+
],
|
|
12
20
|
"type": "module",
|
|
13
21
|
"engines": {
|
|
14
22
|
"node": ">=20"
|
|
@@ -21,6 +29,7 @@
|
|
|
21
29
|
"files": [
|
|
22
30
|
"lib/index.js",
|
|
23
31
|
"vendor",
|
|
32
|
+
"scripts/*.py",
|
|
24
33
|
"cordis.patch.yml",
|
|
25
34
|
"locale/*.json",
|
|
26
35
|
"icon.svg",
|
|
@@ -31,5 +40,8 @@
|
|
|
31
40
|
"bundle": {
|
|
32
41
|
"patch": "./cordis.patch.yml"
|
|
33
42
|
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"pdf-parse": "^2.4.5"
|
|
34
46
|
}
|
|
35
47
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""读取 DXF 文件并输出 JSON(供 DSH dwg-reader 插件调用)。"""
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import ezdxf
|
|
9
|
+
except ImportError as exc:
|
|
10
|
+
print(json.dumps({"error": f"ezdxf 未安装: {exc}"}))
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
|
|
13
|
+
DXF_PATH = sys.argv[1] if len(sys.argv) > 1 else None
|
|
14
|
+
ENTITY_LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 200
|
|
15
|
+
WANT_ENTITIES = sys.argv[3] == "true" if len(sys.argv) > 3 else False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def point_to_list(p):
|
|
19
|
+
if p is None:
|
|
20
|
+
return None
|
|
21
|
+
return [float(p.x), float(p.y), float(p.z)]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
if not DXF_PATH or not os.path.exists(DXF_PATH):
|
|
26
|
+
print(json.dumps({"error": f"文件不存在: {DXF_PATH}"}))
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
doc = ezdxf.readfile(DXF_PATH)
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
print(json.dumps({"error": f"无法读取 DXF: {exc}"}))
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
# 图层
|
|
36
|
+
layers = []
|
|
37
|
+
for layer in doc.layers:
|
|
38
|
+
layers.append({
|
|
39
|
+
"name": layer.dxf.name,
|
|
40
|
+
"colorIndex": layer.dxf.color,
|
|
41
|
+
"lineType": layer.dxf.linetype,
|
|
42
|
+
"frozen": bool(layer.is_frozen()),
|
|
43
|
+
"off": bool(layer.is_off()),
|
|
44
|
+
"locked": bool(layer.is_locked()),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# 块记录
|
|
48
|
+
block_count = len(list(doc.blocks))
|
|
49
|
+
|
|
50
|
+
# 实体统计
|
|
51
|
+
msp = doc.modelspace()
|
|
52
|
+
entities = list(msp)
|
|
53
|
+
entity_type_counts = {}
|
|
54
|
+
entity_details = []
|
|
55
|
+
for ent in entities:
|
|
56
|
+
etype = ent.dxftype() or "UNKNOWN"
|
|
57
|
+
entity_type_counts[etype] = entity_type_counts.get(etype, 0) + 1
|
|
58
|
+
if WANT_ENTITIES and len(entity_details) < ENTITY_LIMIT:
|
|
59
|
+
detail = {
|
|
60
|
+
"type": etype,
|
|
61
|
+
"handle": ent.dxf.handle if hasattr(ent.dxf, 'handle') else None,
|
|
62
|
+
"layer": ent.dxf.layer if hasattr(ent.dxf, 'layer') else None,
|
|
63
|
+
"colorIndex": ent.dxf.color if hasattr(ent.dxf, 'color') else None,
|
|
64
|
+
"lineType": ent.dxf.linetype if hasattr(ent.dxf, 'linetype') else None,
|
|
65
|
+
}
|
|
66
|
+
if etype == "INSERT":
|
|
67
|
+
detail["blockName"] = ent.dxf.name if hasattr(ent.dxf, 'name') else None
|
|
68
|
+
detail["insertionPoint"] = point_to_list(ent.dxf.insert) if hasattr(ent.dxf, 'insert') else None
|
|
69
|
+
entity_details.append(detail)
|
|
70
|
+
|
|
71
|
+
# 图头
|
|
72
|
+
header = {}
|
|
73
|
+
try:
|
|
74
|
+
header_vars = doc.header
|
|
75
|
+
for key in ("$ACADVER", "$DWGCODEPAGE", "$INSUNITS", "$EXTMIN", "$EXTMAX", "$TDCREATE", "$TDUPDATE", "$CLAYER"):
|
|
76
|
+
if key in header_vars:
|
|
77
|
+
val = header_vars[key]
|
|
78
|
+
if key == "$EXTMIN" or key == "$EXTMAX":
|
|
79
|
+
header[key] = point_to_list(val) if val else None
|
|
80
|
+
else:
|
|
81
|
+
header[key] = str(val)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
result = {
|
|
86
|
+
"path": os.path.abspath(DXF_PATH),
|
|
87
|
+
"fileName": os.path.basename(DXF_PATH),
|
|
88
|
+
"fileSizeBytes": os.path.getsize(DXF_PATH),
|
|
89
|
+
"format": "dxf",
|
|
90
|
+
"dxfVersion": doc.dxfversion,
|
|
91
|
+
"header": header,
|
|
92
|
+
"layerCount": len(layers),
|
|
93
|
+
"layers": layers,
|
|
94
|
+
"blockCount": block_count,
|
|
95
|
+
"entityCount": len(entities),
|
|
96
|
+
"entityTypeCounts": entity_type_counts,
|
|
97
|
+
"entities": entity_details,
|
|
98
|
+
"entitiesTruncated": WANT_ENTITIES and len(entities) > ENTITY_LIMIT,
|
|
99
|
+
}
|
|
100
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|