cvpilot-tool/apps/desktop/electron/core/pacs.ts
2024-09-23 17:14:07 +08:00

236 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
};