236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
import { spawn } from "node:child_process";
|
||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||
import log from "electron-log";
|
||
import path from "node:path";
|
||
import FormData from "form-data";
|
||
import { readFile } from "fs/promises";
|
||
import axios from "axios";
|
||
import { InstanceInfo, PatientInfo, SeriesInfo, StudyInfo } from "./pacs.type";
|
||
import { app } from "electron";
|
||
import pLimit from "p-limit";
|
||
|
||
export const OrthancServerRoot = "http://localhost:8042";
|
||
|
||
export const getPacsPath = (
|
||
platform: "macos" | "windows",
|
||
isDevelopment: boolean
|
||
): string => {
|
||
const orthancExecFile = {
|
||
macos: "orthanc-mac-24.8.1/Orthanc",
|
||
windows: "orthanc-win64-1.12.4/Orthanc.exe",
|
||
};
|
||
const basePath = isDevelopment
|
||
? path.join(process.env.VITE_PUBLIC, "../extraResources")
|
||
: path.join(process.resourcesPath, "lib");
|
||
return path.join(basePath, orthancExecFile[platform]);
|
||
};
|
||
|
||
export const runOrthancServer = (pacsPath: string) => {
|
||
if (existsSync(pacsPath)) {
|
||
const configPath = path.join(path.dirname(pacsPath), "config.json");
|
||
const child_process = spawn(pacsPath, [configPath]);
|
||
child_process.stdout.on("data", (data) => log.info(data.toString()));
|
||
child_process.stderr.on("data", (data) => log.error(data.toString()));
|
||
} else {
|
||
console.error("pacsPath is a not exist");
|
||
log.error("pacsPath is a not exist");
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 上传文件到到pacs
|
||
* @param {string} filePath 文件地址
|
||
* @param {string} orthancUrl orthanc的服务地址
|
||
* @returns
|
||
*/
|
||
export const uploadDicomFile = async (
|
||
filePath: string,
|
||
orthancUrl: string = OrthancServerRoot
|
||
): Promise<boolean> => {
|
||
try {
|
||
const buffer = await readFile(filePath);
|
||
const fd = new FormData();
|
||
fd.append("files", buffer);
|
||
const url = `${orthancUrl}/instances`;
|
||
const headers = { "Content-Type": "multipart/form-data" };
|
||
const { status } = await axios.post(url, fd, { headers });
|
||
return status === 200;
|
||
} catch (error) {
|
||
log.error("Failed to upload DICOM file:", error);
|
||
console.error("Failed to upload DICOM file:", error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
export const selectStructuredDicom = async (
|
||
orthancUrl: string = OrthancServerRoot
|
||
) => {
|
||
try {
|
||
const response = await axios.get(`${orthancUrl}/patients`);
|
||
const patientIds: string[] = response.data;
|
||
const patients: PatientInfo[] = [];
|
||
|
||
for (const patientId of patientIds) {
|
||
const patientDetailsResponse = await axios.get<PatientInfo>(
|
||
`${orthancUrl}/patients/${patientId}`
|
||
);
|
||
const patientDetails = patientDetailsResponse.data;
|
||
|
||
const studyIds = patientDetails.Studies;
|
||
const studies: StudyInfo[] = [];
|
||
|
||
for (const studyId of studyIds) {
|
||
const studyDetailsResponse = await axios.get<StudyInfo>(
|
||
`${orthancUrl}/studies/${studyId}`
|
||
);
|
||
const studyDetails = studyDetailsResponse.data;
|
||
|
||
const seriesIds = studyDetails.Series;
|
||
const series: SeriesInfo[] = [];
|
||
|
||
for (const seriesId of seriesIds) {
|
||
const seriesDetailsResponse = await axios.get<SeriesInfo>(
|
||
`${orthancUrl}/series/${seriesId}`
|
||
);
|
||
const seriesDetails = seriesDetailsResponse.data;
|
||
series.push({ ...seriesDetails });
|
||
}
|
||
studies.push({ ...studyDetails, children: series });
|
||
}
|
||
patients.push({ ...patientDetails, children: studies });
|
||
}
|
||
return patients;
|
||
} catch (error) {
|
||
console.error("Error fetching detailed patient information:", error);
|
||
throw error; // or handle it accordingly
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 根据 SeriesInstanceUID 下载序列的所有 DICOM 文件并返回保存的文件夹路径
|
||
* @param seriesInstanceUID 序列的 SeriesInstanceUID
|
||
* @returns 保存的文件夹路径
|
||
*/
|
||
export const downloadSeriesDicomFiles = async (
|
||
seriesInstanceUID: string
|
||
): Promise<string> => {
|
||
try {
|
||
// 使用 Orthanc 的查询接口查找匹配的序列 ID
|
||
const findResponse = await axios.post<string[]>(
|
||
`${OrthancServerRoot}/tools/find`,
|
||
{
|
||
Level: "Series",
|
||
Query: {
|
||
SeriesInstanceUID: seriesInstanceUID,
|
||
},
|
||
}
|
||
);
|
||
|
||
const seriesIds = findResponse.data;
|
||
|
||
if (seriesIds.length === 0) {
|
||
throw new Error("在 Orthanc 中未找到指定的 SeriesInstanceUID。");
|
||
}
|
||
|
||
const seriesId = seriesIds[0]; // 假设只有一个匹配的序列
|
||
|
||
// 获取该序列中的所有实例(DICOM 文件)
|
||
const instancesResponse = await axios.get<string[]>(
|
||
`${OrthancServerRoot}/series/${seriesId}/instances`
|
||
);
|
||
|
||
const instances = instancesResponse.data as unknown as InstanceInfo[];
|
||
|
||
// 创建保存 DICOM 文件的本地目录
|
||
const saveDir = path.join(
|
||
app.getPath("userData"),
|
||
"dicom",
|
||
seriesInstanceUID
|
||
);
|
||
if (!existsSync(saveDir)) {
|
||
mkdirSync(saveDir, { recursive: true });
|
||
}
|
||
|
||
// 并发限制
|
||
const limit = pLimit(5); // 限制同时进行的请求数为 5
|
||
|
||
// 并行下载 DICOM 文件
|
||
await Promise.all(
|
||
instances.map((instance) => {
|
||
const instanceId = instance.ID;
|
||
return limit(async () => {
|
||
const dicomResponse = await axios.get<ArrayBuffer>(
|
||
`${OrthancServerRoot}/instances/${instanceId}/file`,
|
||
{
|
||
responseType: "arraybuffer",
|
||
}
|
||
);
|
||
|
||
const filePath = path.join(saveDir, `${instanceId}.dcm`);
|
||
writeFileSync(filePath, Buffer.from(dicomResponse.data), "binary");
|
||
});
|
||
})
|
||
);
|
||
|
||
console.log(`所有 DICOM 文件已保存至:${saveDir}`);
|
||
return saveDir;
|
||
} catch (error) {
|
||
console.error("下载 DICOM 文件时出错:", error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 根据SeriesInstanceUID获取总的扫描长度
|
||
* @param seriesInstanceUID 序列实例UID
|
||
* @returns 返回总的扫描长度(单位:毫米)
|
||
*/
|
||
export const getTotalScanLength = async (
|
||
seriesInstanceUID: string
|
||
): Promise<number | null> => {
|
||
try {
|
||
// 1. 查找序列ID
|
||
const findResponse = await axios.post(`${OrthancServerRoot}/tools/find`, {
|
||
Level: "Series",
|
||
Query: {
|
||
SeriesInstanceUID: seriesInstanceUID,
|
||
},
|
||
});
|
||
const seriesIds = findResponse.data;
|
||
if (seriesIds.length === 0) {
|
||
console.error("Series not found");
|
||
return null;
|
||
}
|
||
|
||
const seriesId = seriesIds[0];
|
||
|
||
// 2. 获取共享标签,提取SliceThickness
|
||
const tagsResponse = await axios.get(
|
||
`${OrthancServerRoot}/series/${seriesId}/shared-tags`
|
||
);
|
||
const tags = tagsResponse.data;
|
||
const sliceThickness = parseFloat(tags["0018,0050"]["Value"]); // (0018,0050) SliceThickness
|
||
|
||
if (isNaN(sliceThickness)) {
|
||
console.error("SliceThickness not found");
|
||
return null;
|
||
}
|
||
|
||
// 3. 获取实例列表,计算层数
|
||
const instancesResponse = await axios.get(
|
||
`${OrthancServerRoot}/series/${seriesId}/instances`
|
||
);
|
||
const instances = instancesResponse.data;
|
||
const numberOfSlices = instances.length;
|
||
console.log("numberOfSlices", numberOfSlices);
|
||
console.log("sliceThickness", sliceThickness);
|
||
|
||
// 4. 计算总的扫描长度
|
||
const totalLength = Number(sliceThickness) * numberOfSlices;
|
||
return totalLength;
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
return null;
|
||
}
|
||
};
|