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 => { 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( `${orthancUrl}/patients/${patientId}` ); const patientDetails = patientDetailsResponse.data; const studyIds = patientDetails.Studies; const studies: StudyInfo[] = []; for (const studyId of studyIds) { const studyDetailsResponse = await axios.get( `${orthancUrl}/studies/${studyId}` ); const studyDetails = studyDetailsResponse.data; const seriesIds = studyDetails.Series; const series: SeriesInfo[] = []; for (const seriesId of seriesIds) { const seriesDetailsResponse = await axios.get( `${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 => { try { // 使用 Orthanc 的查询接口查找匹配的序列 ID const findResponse = await axios.post( `${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( `${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( `${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; } }; export /** * 根据SeriesInstanceUID获取总的扫描长度 * @param seriesInstanceUID 序列实例UID * @returns 返回总的扫描长度(单位:毫米) */ const getTotalScanLength = async ( seriesInstanceUID: string ): Promise => { 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; } };