Compare commits

...
46 Commits
Author SHA1 Message Date
Ross 134bbf85b6 Add proxy support for SSL certificate fetching in ssl_check utility 2025-11-24 16:44:37 +00:00
Ross Kruger b7242d19de . 2025-11-24 16:35:05 +00:00
Ross 854fd315fa Add standalone SSL check utility with comprehensive certificate validation 2025-11-24 16:25:50 +00:00
Ross ffc4c79def Enhance SSL check command to handle protocol version issues with OpenSSL, adding retry logic for TLS1.2 support. 2025-11-24 16:23:30 +00:00
Ross Kruger 8780a409de . 2025-11-24 16:21:06 +00:00
Ross 2973e86d45 Improve artifact detection logic in build script to prioritize executable files and directories, enhancing robustness and user feedback. 2025-11-24 16:13:09 +00:00
Ross bb0d3d8af1 Enhance SSL check functionality with improved logging and UI feedback 2025-11-24 16:10:04 +00:00
Ross 9c7777323d Add SSL certificate check functionality and UI integration 2025-11-24 15:59:01 +00:00
Ross Kruger 796027a971 . 2025-11-24 15:52:31 +00:00
Ross 3680716d71 Update build process to include certificate directory for SSL verification in frozen builds 2025-11-24 15:44:43 +00:00
Ross Kruger 7528202978 . 2025-11-24 15:43:05 +00:00
Ross 377be258de include cert.pem for SSL verification in frozen builds and update build process to handle CA bundles 2025-11-24 15:30:48 +00:00
Ross 84d18f25e1 add psutil to requirements for enhanced system monitoring 2025-11-24 15:05:14 +00:00
Ross Kruger a0ee79e318 . 2025-11-24 15:04:50 +00:00
Ross 27f4aeec5e add process detection for TCP port usage and improve native mode backend checks in launch_app function 2025-11-24 14:09:06 +00:00
Ross Kruger f1b8b81c76 . 2025-11-24 14:01:02 +00:00
Ross 305e47ba8f disable native mode by default in launch_app function 2025-11-24 13:52:57 +00:00
Ross 6a49f47a5b add native mode checks and fallback for pywebview in UI launch process; create new pywebview module 2025-11-24 13:47:32 +00:00
Ross b40e802627 enhance build process to handle locked artifacts and ensure directory creation 2025-11-24 13:43:15 +00:00
Ross f0f50381ec disable native mode for frozen builds to avoid pythonnet loader issues 2025-11-24 12:40:01 +00:00
Ross 5f2bcb81a4 include pythonnet package and clr_loader in build process for runtime compatibility 2025-11-24 12:24:10 +00:00
Ross f42c1a1839 include pythonnet runtime DLLs in build process to prevent missing dependencies 2025-11-24 12:18:15 +00:00
Ross 094d66b137 rename test binary from 'anon_gui_test' to 'Uploader_test' for consistency 2025-11-24 12:15:00 +00:00
Ross 6366557d20 Merge branch 'main' of git.xkjq.uk:ross/uploader 2025-11-24 12:11:35 +00:00
Ross fa469a79fc fix build process: use absolute paths for icon folder in PyInstaller configuration to prevent runtime errors 2025-11-24 12:11:12 +00:00
Ross Kruger e93e7a8861 . 2025-11-24 11:57:58 +00:00
Ross 6db2a8fcf3 enhance build process: include dicognito package data in PyInstaller build to prevent runtime errors 2025-11-24 11:57:00 +00:00
Ross 21328b53dd refactor logging setup: improve log file path handling for development and frozen application 2025-11-24 11:48:13 +00:00
Ross eb061992a3 enhance build script and logging: add destination option to build command, log output to uploader.log, and improve artifact handling 2025-11-24 11:34:43 +00:00
Ross Kruger a8c48c12f9 v 2025-11-24 11:23:22 +00:00
Ross c735b45a25 refactor login handling: replace inline logic with perform_login function for improved clarity and maintainability 2025-11-24 10:54:42 +00:00
Ross 05c80a8fd7 remove wx and clean up deps 2025-11-24 10:51:20 +00:00
Ross Kruger ae4d127e89 . 2025-11-24 10:35:48 +00:00
Ross 66b4fffb7b tidy up 2024-12-09 11:13:46 +00:00
Ross 8aae29f3e8 Merge branch 'main' of https://git.xkjq.uk/ross/uploader 2024-12-09 09:26:45 +00:00
Ross 15f2e4a6e4 add typer 2024-12-09 09:26:12 +00:00
Ross Kruger 4219680e7a make windows compat 2024-12-09 09:25:48 +00:00
Ross 92289831c6 fix a few issues 2024-12-02 13:33:00 +00:00
Ross e6931f3c8a try with nng 2024-12-02 12:30:46 +00:00
Ross 530f9ed7ea will it compile (on windows) 2024-12-02 11:50:13 +00:00
Ross Kruger 6532801fc1 refactor so multiprocessing works on windows 2024-11-27 15:29:16 +00:00
Ross 242237a0b2 start a nicegui implementation 2024-11-26 22:59:32 +00:00
Ross Kruger 30f8d040c8 start threading 2024-06-21 11:27:40 +01:00
Ross Kruger e52349ab46 add build details 2024-06-21 11:27:31 +01:00
Ross 69371bf9df improve duplicate handling 2024-02-27 22:58:24 +00:00
Ross 2a243c718b fix icon size 2024-02-27 22:01:37 +00:00
23 changed files with 2073 additions and 2163 deletions
+6
View File
@@ -9,4 +9,10 @@ __pycache__/*
build
dist
test/*
*.build
C:/* # Windows paths on linux
uploader.log
-44
View File
@@ -1,44 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['anon_gui.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='anon_gui',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
-6
View File
@@ -1,6 +0,0 @@
@echo off
rem This script was created by Nuitka to execute 'anon_gui.exe' with Python DLL being found.
set PATH=C:\PROGRA~1\PYTHON~1;%PATH%
set PYTHONHOME=C:\Program Files\Python311
"%~dp0anon_gui.exe" %*
BIN
View File
Binary file not shown.
-1463
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
import dicognito.anonymizer
import pydicom
from pathlib import Path
import os
class Anonymizer:
def __init__(self, output_dir):
self.output_dir = output_dir
self.anonymizer = dicognito.anonymizer.Anonymizer()
self.i: int = 0
def reset(self):
self.i = 0
self.anonymizer = dicognito.anonymizer.Anonymizer()
def anonymize_file(self, input_file: Path, output_file: Path | None = None, remove_original=True):
with pydicom.dcmread(input_file) as dataset:
if output_file is None:
new_filepath: Path
# Get next available filename
while os.path.exists(
output_file := self.output_dir.joinpath(f"IMG_{self.i:03}.dcm")
):
self.i += 1
self.anonymizer.anonymize(dataset)
dataset.save_as(output_file)
if remove_original:
os.remove(input_file)
return dataset, output_file
+205
View File
@@ -0,0 +1,205 @@
import shutil
import importlib.util
from pathlib import Path
import PyInstaller.__main__
import typer
from rich import print
import time
from datetime import datetime
app = typer.Typer()
@app.command()
def build(build: bool = True, copy: bool = True, test: bool = False, dest: str | None = None):
"""Windows-focused build script.
- `--test` creates a one-file test binary in `dist\test`.
- `--copy` (default) copies the artifact to the shared network path used previously.
"""
project_root = Path(__file__).resolve().parent
if build:
if test:
print("Building Windows test one-file bundle with PyInstaller")
# On Windows use ';' as add-data separator (src;dest)
# Use absolute path for the icon folder so PyInstaller can find it
icon_src = str(project_root / "icon")
add_data_args = ["--add-data", f"{icon_src};icon"]
# Try to include dicognito package data (release_notes.md) which
# PyInstaller may miss. This prevents runtime FileNotFoundError for
# dicognito/release_notes.md when the package expects the file at runtime.
try:
spec = importlib.util.find_spec("dicognito")
if spec and spec.submodule_search_locations:
dicognito_path = Path(spec.submodule_search_locations[0])
release_notes = dicognito_path / "release_notes.md"
if release_notes.exists():
# PyInstaller on Windows expects 'src;dest'
add_data_args += ["--add-data", f"{str(release_notes)};dicognito"]
else:
# include whole package folder if the single file wasn't found
add_data_args += ["--add-data", f"{str(dicognito_path)};dicognito"]
except Exception as e:
print(f"Warning: couldn't locate dicognito package: {e}")
# Try to include pythonnet runtime DLLs to avoid missing
# Python.Runtime.dll at runtime (used by pythonnet / webview).
add_binary_args = []
try:
spec_py = importlib.util.find_spec("pythonnet")
if spec_py and spec_py.submodule_search_locations:
pn_path = Path(spec_py.submodule_search_locations[0])
runtime_dir = pn_path / "runtime"
if runtime_dir.exists():
for f in runtime_dir.rglob("*.dll"):
# PyInstaller on Windows expects 'src;dest'
add_binary_args += ["--add-binary", f"{str(f)};pythonnet\\runtime"]
# also include the full pythonnet package folder as data so
# package-relative lookups succeed at runtime
add_data_args += ["--add-data", f"{str(pn_path)};pythonnet"]
# try to include clr_loader package as well
try:
spec_clr = importlib.util.find_spec("clr_loader")
if spec_clr and spec_clr.submodule_search_locations:
clr_path = Path(spec_clr.submodule_search_locations[0])
add_data_args += ["--add-data", f"{str(clr_path)};clr_loader"]
except Exception:
pass
except Exception as e:
print(f"Warning: couldn't locate pythonnet runtime: {e}")
# Ensure dist/work/spec directories exist
dist_dir = project_root / "dist" / "test"
work_dir = project_root / "build" / "test"
spec_dir = project_root / "build" / "specs"
dist_dir.mkdir(parents=True, exist_ok=True)
work_dir.mkdir(parents=True, exist_ok=True)
spec_dir.mkdir(parents=True, exist_ok=True)
# Decide build name and try to remove any locked previous artifact.
base_name = "Uploader_test"
output_exe = dist_dir / f"{base_name}.exe"
def try_remove_with_retries(path: Path, retries: int = 5, delay: float = 0.5) -> bool:
"""Try to remove a file with retries and backoff. Returns True if removed or not present."""
if not path.exists():
return True
for attempt in range(retries):
try:
path.unlink()
print(f"Removed existing file: {path}")
return True
except PermissionError:
# Try to move/rename it out of the way
try:
backup = path.with_suffix(f".old.{int(time.time())}")
path.rename(backup)
print(f"Renamed locked file to: {backup}")
return True
except Exception:
pass
print(f"File locked, retrying in {delay} seconds ({attempt+1}/{retries})")
time.sleep(delay)
delay *= 1.5
except Exception as e:
print(f"Failed to remove existing file {path}: {e}")
return False
return False
name_used = base_name
if not try_remove_with_retries(output_exe):
# If the previous artifact is locked, fall back to a unique name to avoid build failure
ts = datetime.now().strftime("%Y%m%dT%H%M%S")
name_used = f"{base_name}_{ts}"
print(f"Previous artifact appears locked; building as {name_used} to avoid PermissionError")
# Ensure cert folder is included for test builds as well (if present)
cert_dir = project_root / "cert"
if cert_dir.exists() and cert_dir.is_dir():
arg = f"{str(cert_dir)};cert"
# Avoid adding duplicate entries
if arg not in add_data_args:
add_data_args += ["--add-data", arg]
PyInstaller.__main__.run([
str(project_root / "nice.py"),
"--onefile",
"--name",
name_used,
"--distpath",
str(dist_dir),
"--workpath",
str(work_dir),
"--specpath",
str(spec_dir),
] + add_data_args + add_binary_args + [
# add some common hidden imports that PyInstaller sometimes misses
"--hidden-import",
"bs4",
"--hidden-import",
"requests",
"--hidden-import",
"nicegui",
])
else:
print("Building using spec: nice_uploader.spec")
PyInstaller.__main__.run([str(project_root / "nice_uploader.spec")])
if copy:
# Windows-only default destination used previously in the repo
default_dest = Path(r"\\ict\Go\RCH\Shared\Dragon-Xray\rad-tools\uploader\Uploader")
artifact_dir = project_root / "dist" / ("test" if test else "")
if not artifact_dir.exists():
artifact_dir = project_root / "dist"
# Look for a matching artifact. Prefer exe files named Uploader*.exe,
# then any .exe, then directories named Uploader* (onedir builds).
# Exclude obvious non-artifacts like log files.
candidates = []
# Uploader*.exe (preferred)
candidates.extend(sorted(artifact_dir.glob("Uploader*.exe")))
# any .exe
candidates.extend(sorted(artifact_dir.glob("*.exe")))
# onedir: directories starting with Uploader
candidates.extend([p for p in sorted(artifact_dir.glob("Uploader*")) if p.is_dir()])
# Filter out files that look like logs or non-executables
def is_valid_artifact(p: Path) -> bool:
if not p.exists():
return False
if p.is_file():
# require .exe for files (Windows builds)
return p.suffix.lower() == ".exe"
if p.is_dir():
return True
return False
valid = [p for p in candidates if is_valid_artifact(p)]
if not valid:
print(f"No build artifact (.exe or Uploader directory) found in {artifact_dir}, skipping copy")
return
artifact = valid[0]
dest_path = Path(dest) if dest else default_dest
dest_path.mkdir(parents=True, exist_ok=True)
if artifact.is_file():
target = dest_path / artifact.name
print(f"Copying {artifact} -> {target}")
shutil.copy2(artifact, target)
else:
target = dest_path / artifact.name
print(f"Copying directory {artifact} -> {target}")
if target.exists():
shutil.rmtree(target)
shutil.copytree(artifact, target)
if __name__ == "__main__":
app()
+21
View File
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDhzCCAm+gAwIBAgIQHLnU4uDoKaJAaqa5Yi/NoTANBgkqhkiG9w0BAQUFADBW
MQswCQYDVQQGEwJHQjERMA8GA1UECBMIQ29ybndhbGwxFTATBgNVBAoTDENvcm53
YWxsIE5IUzEdMBsGA1UEAxMUQ29ybndhbGwgSUNUIFJvb3QgQ0EwHhcNMTMwNjI2
MDkwNjExWhcNMzMwNjI2MDkxNjA1WjBWMQswCQYDVQQGEwJHQjERMA8GA1UECBMI
Q29ybndhbGwxFTATBgNVBAoTDENvcm53YWxsIE5IUzEdMBsGA1UEAxMUQ29ybndh
bGwgSUNUIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCm
m8jF64E3wkxhf2BLfD3lT3v9RebGhJ684jtv0A6qL2BTe/Co4SJ/obYiDw82K4Vw
1O+jwIg77hVrH/YaCsjPiefxxwL/T0bSURlXq5Q8pCQaHjd1iH1JNY1WfW0Ywkni
a7xbm6/2+zwsyTHfhuSU+zmSQxejOX+Ffz6MWZSb+JjppOH6OBKTHGw69JIXcAUu
wT3GSAdYyYKgP6i6jfsEeJkq+s/hd6cVxFWPrWJJaGtfIvLZT38vZeitYDYT9Ad0
gIQPDyLH35tuNszFWkU5DA/U6xXvWkAV7Mq/EaGSVHckGeQiGAQNmV74KugqpcC/
9reOFKEbc6qvDdGaASjZAgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8E
BTADAQH/MB0GA1UdDgQWBBShR/WOiN4mvXi2tYLGtbeAaCXE7DAQBgkrBgEEAYI3
FQEEAwIBADANBgkqhkiG9w0BAQUFAAOCAQEAEMca1BLnKJIXRZhJUTfLrSWtQDgV
T1jGnHFKngPxWUB1u0fVwpnqeuHSWQ2qkNvEZoBQFbJhEny83r8thLamJB1UHEyj
yhWrtjif7dne3LwzLtmiMZIKPjeyCa+zzl5YW+lUenQErl48PC7sJc5gq4lZajOo
fsEtw5zZLWFqXE/KJNt4vrGhQ0SplphbOzau7VrFIFciiKHHHw3rnV5byjqYDy/Z
IJytye488Pi75lBKq8pWW6YbUFmKly5bqF6Q8oicd9y0/9GGmTZGrzp1WbspYF9Y
lc2p6bkhfQ1f7Q+8/sF0D/sGfjfo099OlRhc881Mwt5p8mEZtJTxk3SZlQ==
-----END CERTIFICATE-----
+19
View File
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJjCCAg6gAwIBAgIIUW6l3kYeVMEwDQYJKoZIhvcNAQELBQAwMTEOMAwGA1UE
ChMFQ2lzY28xHzAdBgNVBAMTFkNpc2NvIFVtYnJlbGxhIFJvb3QgQ0EwHhcNMTYw
NjI4MTUzNzUzWhcNMzYwNjI4MTUzNzUzWjAxMQ4wDAYDVQQKEwVDaXNjbzEfMB0G
A1UEAxMWQ2lzY28gVW1icmVsbGEgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAO7ZjfBSCaz5EMYSiWYoXjHPP/w7xFT4bXa82lOZ9CJJXDQw
bZpBdmuqX9UWo769LIAaSUvkYEeZqcTsjrx/7juPKoOErhJY0cPK12LU9PbHXqEd
XESIqBjdOC5oiIFHhTAKuuKRlL7rhPYkYhZtgdll4h0FLIG+xNsMVfzJb7z69X8Y
vF9r1drLkd7oR2xHuRkXgzeblFVpF+DRF7WXNhLy0By38ZxtClxYUSitdz53W0ic
maelG7EyCVNVxARxn5waaphRvki1hkuqqrm3JdlV165zAOdSz3JKzRISQinCTQuT
+RK/w0qLsDTyOVO/mEIVWLXu/Z1NtuXgj/jhegcCAwEAAaNCMEAwDgYDVR0PAQH/
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFENzAN4kukAaQFQsfXzV
AEiJDHCkMA0GCSqGSIb3DQEBCwUAA4IBAQBIEoceSPZLmo5sLmgDfQA+Fq5BKztL
qg8aAvZdrbdMEKEBr1RDB0OAhuPcaaVxZi6Hjyql1N999Zmp8qIw/lLTt3VSTmEa
29uPgjdMGLl9KyfZjARiA/PPvPdHTwg7TMJOet+w7P5nWabLNW55+Wc/JzCSFE30
+0Kdz/jojxlA/8t0xYLCdS2UK7zC4kuAbojHLJDbIQO3HeEWwVmg4FO89AHVvC4R
Y+V0t7SaEradv6tPG9DHX7PLwjQ/Xs95NGDIJTeFwCRqYUlBu9iZjIvKba0e0tST
Vuyw2+P2HuWazjBPawGrbfyw+uO3KO4WnNGjMutJJ920o8B5M8gW1+Ye
-----END CERTIFICATE-----
-65
View File
@@ -1,65 +0,0 @@
import typer
import requests
app = typer.Typer()
from bs4 import BeautifulSoup
# client.py
LOGIN_URL = "https://www.penracourses.org.uk/accounts/login"
# ADD_URL = 'http://localhost/add'
@app.command()
def login(user: str, password: str):
rqst = requests.session()
rsp = rqst.get(LOGIN_URL)
token = BeautifulSoup(rsp.content, "html.parser").find("input").attrs["value"]#, attr={"name": "csrfmiddlewaretoken"}).attrs("value")
#token = rsp.cookies["csrftoken"]
#header = {"X-CSRFToken": token}
#cookies = {"csrftoken": token}
data = {
"username": user,
"password": password,
"csrfmiddlewaretoken": token,
"next": "/",
}
print(data)
rqst.headers["Referer"] = LOGIN_URL
rsp = rqst.post(
LOGIN_URL,
data=data,
#headers=header,
#cookies=cookies
)
soup = BeautifulSoup(rsp.content, "html.parser")
print(token)
print(rqst.headers)
print(rqst.cookies)
login_success = False
if soup.find("button") and soup.find("button").find(string="Login"):
print("login fail")
pass
else:
print("login success")
login_success = True
#print(rqst.headers)
#print(rsp.content)
#new = rqst.get(
# "https://www.penracourses.org.uk/api/atlas/get_cases_user"
#)
#print(new)
#print(new.content)
if __name__ == "__main__":
app()
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "launcher"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nng = "1.0.1"
#rfd = "0.14.1"
#toml = "0.8.13"
#serde = { version = "1.0", features = ["derive"] }
+81
View File
@@ -0,0 +1,81 @@
use nng::{Protocol, Socket, Error};
use std::process::Command;
//use rfd::MessageDialog;
//use serde::Deserialize;
//use std::fs;
//use std::path::PathBuf;
//#[derive(Debug, Deserialize)]
//struct Settings {
// cris_tools_path: PathBuf,
// port: toml::Value,
//}
//fn load_settings() -> Result<Settings, toml::de::Error> {
// // Read the entire contents of the file
// let contents = fs::read_to_string("uploader.toml")
// .expect("Failed to read settings file");
//
// // Parse the file contents and deserialize into the Settings struct
// toml::from_str(&contents)
//}
fn main() {
//let settings = match load_settings() {
// Ok(settings) => settings,
// Err(e) => {
// eprintln!("Failed to load settings: {:?}", e);
// std::process::exit(1);
// }
//};
// Create a socket of type REQ (request)
let socket = Socket::new(Protocol::Pair0).expect("Failed to create socket");
// Connect to the NNG server
//match socket.dial(format!("tcp://localhost:{}", settings.port).as_str()) {
match socket.dial(format!("tcp://localhost:9976").as_str()) {
Ok(_) => {
println!("Connected to server");
}
Err(e) => {
println!("Failed to connect to server: {:?}", e);
println!("Launching uploader tool");
let output = Command::new("Uploader.exe")
.spawn();
std::process::exit(1);
}
}
// Get the command line argument
//let arg = std::env::args().nth(1).expect("Missing argument");
//let arg = match std::env::args().nth(1) {
// Some(arg) => arg,
// None => {
// eprintln!("Missing argument");
// std::process::exit(1);
// }
//};
// Send the argument to the server
//let message = "run/".to_string() + &arg;
let message = "loaded".to_string();
println!("Send message: {}", message);
socket.send(message.as_bytes()).expect("Failed to send message");
// Receive the response from the server
match socket.recv() {
Ok(response) => {
let response = String::from_utf8(response.to_vec()).expect("Failed to receive response");
println!("Response: {}", response);
}
Err(Error::TimedOut) => {
println!("Failed to receive response: Timeout");
}
Err(e) => {
println!("Failed to receive response: {:?}", e);
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import platform
from pathlib import Path
from typing import Optional
from nicegui import events, ui
class local_file_picker(ui.dialog):
def __init__(self, directory: str, *,
upper_limit: Optional[str] = ..., multiple: bool = False, show_hidden_files: bool = False) -> None:
"""Local File Picker
This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.
:param directory: The directory to start in.
:param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
:param multiple: Whether to allow multiple files to be selected.
:param show_hidden_files: Whether to show hidden files.
"""
super().__init__()
self.path = Path(directory).expanduser()
if upper_limit is None:
self.upper_limit = None
else:
self.upper_limit = Path(directory if upper_limit == ... else upper_limit).expanduser()
self.show_hidden_files = show_hidden_files
with self, ui.card():
self.add_drives_toggle()
self.grid = ui.aggrid({
'columnDefs': [{'field': 'name', 'headerName': 'File'}],
'rowSelection': 'multiple' if multiple else 'single',
}, html_columns=[0]).classes('w-96').on('cellDoubleClicked', self.handle_double_click)
with ui.row().classes('w-full justify-end'):
ui.button('Cancel', on_click=self.close).props('outline')
ui.button('Ok', on_click=self._handle_ok)
self.update_grid()
def add_drives_toggle(self):
if platform.system() == 'Windows':
import win32api
drives = win32api.GetLogicalDriveStrings().split('\000')[:-1]
self.drives_toggle = ui.toggle(drives, value=drives[0], on_change=self.update_drive)
def update_drive(self):
self.path = Path(self.drives_toggle.value).expanduser()
self.update_grid()
def update_grid(self) -> None:
paths = list(self.path.glob('*'))
if not self.show_hidden_files:
paths = [p for p in paths if not p.name.startswith('.')]
paths.sort(key=lambda p: p.name.lower())
paths.sort(key=lambda p: not p.is_dir())
self.grid.options['rowData'] = [
{
'name': f'📁 <strong>{p.name}</strong>' if p.is_dir() else p.name,
'path': str(p),
}
for p in paths
]
if self.upper_limit is None and self.path != self.path.parent or \
self.upper_limit is not None and self.path != self.upper_limit:
self.grid.options['rowData'].insert(0, {
'name': '📁 <strong>..</strong>',
'path': str(self.path.parent),
})
self.grid.update()
def handle_double_click(self, e: events.GenericEventArguments) -> None:
self.path = Path(e.args['data']['path'])
if self.path.is_dir():
self.update_grid()
else:
self.submit([str(self.path)])
async def _handle_ok(self):
rows = await self.grid.get_selected_rows()
self.submit([r['path'] for r in rows])
+92
View File
@@ -0,0 +1,92 @@
import platform
from pathlib import Path
from typing import Optional
from nicegui import events, ui
class local_folder_picker(ui.dialog):
def __init__(self, directory: str, *,
upper_limit: Optional[str] = ..., multiple: bool = False, show_hidden_files: bool = False) -> None:
"""Local File Picker
This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.
:param directory: The directory to start in.
:param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
:param multiple: Whether to allow multiple files to be selected.
:param show_hidden_files: Whether to show hidden files.
"""
super().__init__()
self.path = Path(directory).expanduser()
if upper_limit is None:
self.upper_limit = None
else:
self.upper_limit = Path(directory if upper_limit == ... else upper_limit).expanduser()
self.show_hidden_files = show_hidden_files
with self, ui.card():
self.add_drives_toggle()
self.grid = ui.aggrid({
'columnDefs': [{'field': 'name', 'headerName': 'File'}],
'rowSelection': 'multiple' if multiple else 'single',
}, html_columns=[0]).classes('w-96').on('cellDoubleClicked', self.handle_double_click)
with ui.row().classes('w-full justify-end'):
ui.button('Cancel', on_click=self.close).props('outline')
ui.button('Ok', on_click=self._handle_ok)
self.update_grid()
def add_drives_toggle(self):
if platform.system() == 'Windows':
import win32api
drives = win32api.GetLogicalDriveStrings().split('\000')[:-1]
self.drives_toggle = ui.toggle(drives, value=drives[0], on_change=self.update_drive)
def update_drive(self):
self.path = Path(self.drives_toggle.value).expanduser()
self.update_grid()
def update_grid(self) -> None:
paths = list(self.path.glob('*'))
if not self.show_hidden_files:
paths = [p for p in paths if not p.name.startswith('.')]
paths.sort(key=lambda p: p.name.lower())
paths.sort(key=lambda p: not p.is_dir())
self.grid.options['rowData'] = [
{
'name': f'📁 <strong>{p.name}</strong>' if p.is_dir() else p.name,
'path': str(p),
}
for p in paths
]
if (self.upper_limit is None and self.path != self.path.parent) or \
(self.upper_limit is not None and self.path != self.upper_limit):
self.grid.options['rowData'].insert(0, {
'name': '📁 <strong>..</strong>',
'path': str(self.path.parent),
})
self.grid.update()
def handle_double_click(self, e: events.GenericEventArguments) -> None:
self.path = Path(e.args['data']['path'])
if self.path.is_dir():
self.update_grid()
else:
self.submit(self.path.parent)
async def _handle_ok(self):
rows = await self.grid.get_selected_rows()
if not rows:
self.submit(self.path)
else:
p = Path(rows[0]['path'])
if p.is_dir():
self.submit(p)
else:
self.submit(p.parent)
+1169
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import importlib.util
from pathlib import Path
here = Path(__file__).resolve().parent
project_root = here
# Prepare datas and binaries lists; include icon resources
datas = [
(str(project_root / 'icon'), 'icon'),
(str(project_root / 'icon' / 'icon1.png'), 'icon'),
# include user-supplied CA bundle folder (all files) so frozen app can use it
(str(project_root / 'cert'), 'cert')
]
binaries = []
# Try to include pythonnet runtime DLLs so Python.Runtime.dll is available
try:
spec_py = importlib.util.find_spec("pythonnet")
if spec_py and spec_py.submodule_search_locations:
pn_path = Path(spec_py.submodule_search_locations[0])
runtime_dir = pn_path / "runtime"
if runtime_dir.exists():
for f in runtime_dir.rglob("*.dll"):
# destination folder inside the frozen app
binaries.append((str(f), "pythonnet/runtime"))
except Exception:
# best-effort; if we can't locate pythonnet at build time, user can add it manually
pass
a = Analysis(
['nice.py'],
pathex=[str(project_root)],
binaries=binaries,
datas=datas,
hiddenimports=['bs4', 'requests', 'nicegui', 'pynng'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='nice_uploader',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+3
View File
@@ -0,0 +1,3 @@
import webview
webview.create_window('Hello world', 'https://pywebview.flowrl.com/hello')
webview.start()
+4 -2
View File
@@ -1,12 +1,14 @@
typer
wxpython
pydicom
dicognito
python-datauri
bs4
requests
zmq
pynng
blake3
loguru
dicom2jpg
pyinstaller
nicegui
pywebview
psutil
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""Standalone SSL check utility to be frozen for testing.
Usage:
python ssl_check.py --url https://www.penracourses.org.uk --cert-dir ./cert
What it does:
- Runs `openssl s_client -showcerts` against the host (if openssl on PATH).
- Extracts PEM cert blocks from the output and inspects each with `openssl x509`.
- Loads cert files from --cert-dir (PEM or DER) and converts DER to PEM as needed.
- Compares server chain Issuers with Subjects of bundled certs and reports which CA certs are missing.
This script is intentionally small and self-contained so it can be frozen with PyInstaller
for quick testing on Windows where the system openssl may be too old.
"""
from __future__ import annotations
import argparse
import base64
import re
import shutil
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List, Tuple
def find_openssl() -> str | None:
return shutil.which("openssl")
def run_openssl_s_client(host: str, port: int = 443, tls_flag: str | None = None, timeout: int = 60) -> str:
openssl = find_openssl()
if not openssl:
raise RuntimeError("openssl not found on PATH")
cmd = [openssl, "s_client", "-showcerts", "-connect", f"{host}:{port}", "-servername", host]
if tls_flag:
cmd.append(tls_flag)
# give a newline so s_client won't wait for interactive input
proc = subprocess.run(cmd, input="\n", capture_output=True, text=True, timeout=timeout)
return proc.stdout + "\n" + proc.stderr
def fetch_cert_via_proxy(host: str, port: int, proxy: str, timeout: int = 20) -> List[str]:
"""Connect to proxy, issue CONNECT, perform TLS handshake and return PEM of the leaf cert."""
from urllib.parse import urlparse
import socket
import ssl
import base64
parsed = urlparse(proxy)
proxy_host = parsed.hostname
proxy_port = parsed.port or (443 if parsed.scheme == 'https' else 80)
auth_header = None
if parsed.username:
user = parsed.username
pwd = parsed.password or ""
cred = f"{user}:{pwd}".encode("utf-8")
auth_header = base64.b64encode(cred).decode("ascii")
s = socket.create_connection((proxy_host, proxy_port), timeout=timeout)
try:
connect_req = f"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n"
if auth_header:
connect_req += f"Proxy-Authorization: Basic {auth_header}\r\n"
connect_req += "\r\n"
s.sendall(connect_req.encode("ascii"))
# read response headers
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if len(resp) > 65536:
break
header = resp.split(b"\r\n\r\n", 1)[0].decode("ascii", errors="ignore")
# Simple status check
first_line = header.splitlines()[0] if header.splitlines() else ""
if not first_line.startswith("HTTP/") or ("200" not in first_line):
raise RuntimeError(f"Proxy CONNECT failed: {first_line}")
# Wrap the socket with SSL and fetch peer cert (leaf)
ctx = ssl.create_default_context()
with ctx.wrap_socket(s, server_hostname=host) as ss:
der = ss.getpeercert(True)
pem = ssl.DER_cert_to_PEM_cert(der)
return [pem]
except Exception:
s.close()
raise
def extract_pem_blocks(s: str) -> List[str]:
pattern = re.compile(r"-----BEGIN CERTIFICATE-----(?:.|\n)*?-----END CERTIFICATE-----", re.M)
return pattern.findall(s)
def write_pems_to_temp(pems: List[str]) -> List[Path]:
out = []
for i, pem in enumerate(pems):
f = Path(tempfile.gettempdir()) / f"ssl_check_cert_{i}.pem"
f.write_text(pem)
out.append(f)
return out
def inspect_cert(path: Path) -> Tuple[str, str, str]:
"""Return (subject, issuer, fingerprint) by calling openssl x509."""
openssl = find_openssl()
if not openssl:
raise RuntimeError("openssl not found on PATH")
proc = subprocess.run([openssl, "x509", "-in", str(path), "-noout", "-subject", "-issuer", "-fingerprint"], capture_output=True, text=True)
out = proc.stdout + proc.stderr
subj = re.search(r"subject=\s*(.*)", out)
issu = re.search(r"issuer=\s*(.*)", out)
fp = re.search(r"Fingerprint=([A-F0-9:]+)", out)
return (subj.group(1).strip() if subj else "", issu.group(1).strip() if issu else "", fp.group(1).strip() if fp else "")
def load_cert_files_from_dir(cert_dir: Path) -> List[Path]:
files = []
if not cert_dir.exists():
return files
for pattern in ("*.pem", "*.crt", "*.cer"):
files.extend(sorted(cert_dir.glob(pattern)))
out = []
for f in files:
data = f.read_bytes()
if b"-----BEGIN CERTIFICATE-----" in data:
# already PEM
out.append(f)
continue
# assume DER -> convert to PEM
b64 = base64.encodebytes(data).decode("ascii")
pem = "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n"
tmp = Path(tempfile.gettempdir()) / f"ssl_check_bundle_{f.name}.pem"
tmp.write_text(pem)
out.append(tmp)
return out
def main(argv: List[str]) -> int:
p = argparse.ArgumentParser()
p.add_argument("--url", required=True, help="URL or host to check (eg https://www.penracourses.org.uk)")
p.add_argument("--cert-dir", default="cert", help="Directory with bundled certs")
p.add_argument("--force-tls12", action="store_true", help="Force -tls1_2 on openssl if initial attempt failed")
args = p.parse_args(argv)
# parse host/port
from urllib.parse import urlparse
parsed = urlparse(args.url if "//" in args.url else "//" + args.url)
host = parsed.hostname or args.url
port = parsed.port or 443
openssl = find_openssl()
if not openssl:
print("openssl not found on PATH; please install Git for Windows or OpenSSL and re-run.")
# we still attempt Python fallback but chain won't be available
else:
print(f"Using openssl: {openssl}")
out = ""
if openssl:
try:
out = run_openssl_s_client(host, port)
if "tlsv1 alert protocol version" in out or "SSL23_GET_SERVER_HELLO" in out:
print("Server rejected legacy TLS; retrying with -tls1_2")
out2 = run_openssl_s_client(host, port, tls_flag="-tls1_2")
out = out + "\n--- retry result ---\n" + out2
except subprocess.TimeoutExpired:
print("OpenSSL check timed out")
except Exception as e:
print(f"OpenSSL check failed: {e}")
# If openssl couldn't fetch a chain (or wasn't available), attempt a proxy-aware
# fetch if HTTPS_PROXY or HTTP_PROXY is set. This helps in corporate networks
# where direct TLS is blocked and a proxy must be used.
if not extract_pem_blocks(out):
proxy = None
for key in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"):
proxy = os.environ.get(key)
if proxy:
break
if proxy:
try:
print(f"Attempting proxy CONNECT via {proxy}")
pems = fetch_cert_via_proxy(host, port, proxy)
if pems:
print(f"Fetched {len(pems)} PEM(s) via proxy")
server_paths = write_pems_to_temp(pems)
server_info = [inspect_cert(p) for p in server_paths]
print("Server chain (proxy fetched):")
for i, (s, iss, fp) in enumerate(server_info):
print(f"[{i}] SUBJECT: {s}")
print(f" ISSUER: {iss}")
print(f" FP: {fp}")
else:
print("Proxy CONNECT succeeded but no certs found")
except Exception as e:
print(f"Proxy CONNECT fetch failed: {e}")
# extract certs from openssl output if any
pems = extract_pem_blocks(out)
if pems:
print(f"Found {len(pems)} PEM blocks in openssl output")
server_paths = write_pems_to_temp(pems)
server_info = [inspect_cert(p) for p in server_paths]
print("Server chain:")
for i, (s, iss, fp) in enumerate(server_info):
print(f"[{i}] SUBJECT: {s}")
print(f" ISSUER: {iss}")
print(f" FP: {fp}")
else:
print("No certificate chain found from openssl (server may have rejected the handshake or openssl is incompatible)")
server_info = []
# load bundled certs
cert_dir = Path(args.cert_dir)
bundled = load_cert_files_from_dir(cert_dir)
print(f"Found {len(bundled)} bundled cert files in {cert_dir}")
bundled_info = [inspect_cert(p) for p in bundled]
subjects = {info[0]: p for info, p in zip(bundled_info, bundled)}
# Compare: ensure each server issuer is present in bundled subjects
missing = set()
for s, iss, fp in server_info:
if iss and iss not in subjects:
missing.add(iss)
if not server_info:
print("No server certs to compare; check network/openssl compatibility")
return 2
if not missing:
print("All server issuers are present in the bundled certs. OK")
return 0
else:
print("Missing issuer certificates in bundled certs:")
for m in missing:
print(" - ", m)
return 3
if __name__ == "__main__":
try:
rc = main(sys.argv[1:])
except Exception as e:
print(f"ssl_check failed: {e}")
rc = 1
sys.exit(rc)
+4 -4
View File
@@ -2,7 +2,7 @@
a = Analysis(
['anon_gui.py'],
['ssl_check.py'],
pathex=[],
binaries=[],
datas=[],
@@ -12,6 +12,7 @@ a = Analysis(
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
@@ -21,18 +22,17 @@ exe = EXE(
a.binaries,
a.datas,
[],
name='anon_gui',
name='ssl_check',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='icon\\icon1.ico'
)
+6
View File
@@ -0,0 +1,6 @@
PS T:\rad-tools\uploader\Uploader_test> .\ssl_check.exe --url http://www.penracourses.org.uk --cert-dir ./cert
Using openssl: C:\Sybase\OCS-15_0\bin\openssl.EXE
Server rejected legacy TLS; retrying with -tls1_2
No certificate chain found from openssl (server may have rejected the handshake or openssl is incompatible)
Found 2 bundled cert files in cert
No server certs to compare; check network/openssl compatibility
-193
View File
@@ -1,193 +0,0 @@
import webview
import time
import sys
from datauri import DataURI
from pathlib import Path
import pydicom
import dicognito.anonymizer
import typer
from typing import List
from watchdog.observers import Observer
# from watchdog.events import LoggingEventHandler
from watchdog.events import PatternMatchingEventHandler
anonymizer = dicognito.anonymizer.Anonymizer()
app = typer.Typer()
def anonymize_file(filepath):
try:
with pydicom.dcmread(filepath) as dataset:
anonymizer.anonymize(dataset)
p = Path(filepath)
new_filename = Path(p.parent, f"{p.stem}_anon{p.suffix}")
dataset.save_as(new_filename)
return new_filename
except:
# Cannot anonymise so just return the same path
return filepath
class PythonJavascriptApi:
def __init__(self):
self.cancel_heavy_stuff_flag = False
def init(self):
response = {"message": "Hello from Python {0}".format(sys.version)}
return response
def sayHelloTo(self, name):
response = {"message": "Hello {0}!".format(name)}
return response
def error(self):
raise Exception("This is a Python exception")
class Watcher:
def __init__(self, window, anonymise: bool) -> None:
self.window = window
self.anonymise = anonymise
def on_created(self, event):
print(f"hey, {event.src_path} has been created!")
path = event.src_path
if self.anonymise:
path = anonymize_file(path)
try:
file_uri = DataURI.from_file(path)
except FileNotFoundError:
print("File deleted...")
return
file_name = Path(path).name
self.injectFile(file_uri, file_name)
def on_deleted(self, event):
print(f"what the f**k! Someone deleted {event.src_path}!")
def on_modified(self, event):
print(f"hey buddy, {event.src_path} has been modified")
def on_moved(self, event):
print(f"ok ok ok, someone moved {event.src_path} to {event.dest_path}")
def injectFile(self, file_uri: DataURI, file_name: str):
current_url = webview.windows[0].get_current_url()
if current_url == "https://www.penracourses.org.uk/rapids/create/":
print(f"inject {file_name}")
self.window.evaluate_js(
f"""
function urltoFile(url, filename, mimeType){{
return (fetch(url)
.then(function(res){{ return res.arrayBuffer(); }})
.then(function(buf){{return new File([buf], filename,{{type:mimeType}});}})
);
}}
$(document).ready(() => {{
//Usage example:
urltoFile('{file_uri}', '{file_name}', '{file_uri.mimetype}')
.then(function(file){{
console.log(file);
//inputs = $("#rapid-form input[type=file][id^=id_images-]");
inputs = extendInputs(1);
console.log(inputs);
el = inputs.get(0);
console.log(el);
let dT = new DataTransfer();
dT.clearData();
dT.items.add(file);
el.files = dT.files;
// Manually fire the change event (qt doesn't seem to...)
$(el).change();
}});
}});
"""
)
@app.command()
def anatomy(filepath: str):
raise NotImplementedError
@app.command()
def rapid(watch_path: Path = "/home/ross/watch", anonymise: bool = True):
api = PythonJavascriptApi()
window = webview.create_window(
"Upload Rapid", "https://www.penracourses.org.uk/rapids/create/", js_api=api
)
# def on_loaded():
# print('DOM is ready')
# current_url = webview.windows[0].get_current_url()
# if current_url == "https://www.penracourses.org.uk/rapids/create/":
# #window.evaluate_js("alert('tent')")
# for file_uri, file_name in uri_name_list:
# #time.sleep(2)
# ## unsubscribe event listener
# #webview.windows[0].loaded -= on_loaded
# #webview.windows[0].load_url('https://google.com')
# window.loaded += on_loaded
# window = upload_rapid(["/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"])
patterns = ["*"]
ignore_patterns = ["*anon*"]
ignore_directories = False
case_sensitive = True
my_event_handler = PatternMatchingEventHandler(
patterns, ignore_patterns, ignore_directories, case_sensitive
)
watcher = Watcher(window, anonymise)
my_event_handler.on_created = watcher.on_created
my_event_handler.on_deleted = watcher.on_deleted
my_event_handler.on_modified = watcher.on_modified
my_event_handler.on_moved = watcher.on_moved
my_observer = Observer()
Path(watch_path).mkdir(parents=True, exist_ok=True)
my_observer.schedule(my_event_handler, watch_path, recursive=True)
my_observer.start()
# try:
# while True:
# time.sleep(1)
# except KeyboardInterrupt:
# my_observer.stop()
# my_observer.join()
webview.start(None, window, debug=True, gui="qt")
# f = "/media/ross/f6200b79-c343-4882-a245-28fc06d43006/Downloads/temp/breasts.jpg"
f = "/home/ross/Downloads/DICOM CT/DICOM CXR/STD1/SER1/CXR.dcm"
if __name__ == "__main__":
app()
-384
View File
@@ -1,384 +0,0 @@
# ===================================================================
# imViewer-Simple.py
#
# An example program that opens uncompressed DICOM images and
# converts them via numPy and PIL to be viewed in wxWidgets GUI
# apps. The conversion is currently:
#
# pydicom->NumPy->PIL->wxPython.Image->wxPython.Bitmap
#
# Gruesome but it mostly works. Surely there is at least one
# of these steps that could be eliminated (probably PIL) but
# haven't tried that yet and I may want some of the PIL manipulation
# functions.
#
# This won't handle RLE, embedded JPEG-Lossy, JPEG-lossless,
# JPEG2000, old ACR/NEMA files, or anything wierd. Also doesn't
# handle some RGB images that I tried.
#
# Have added Adit Panchal's LUT code. It helps a lot, but needs
# to be further generalized. Added test for window and/or level
# as 'list' type - crude, but it worked for a bunch of old MR and
# CT slices I have.
#
# Testing: moderate
# Tested on Windows 7 and MacOS 10.13 using numpy 1.15.2,
# Pillow 5.2.0, Python 2.7.13 / 3.6.5, and wxPython 4.0.3
#
# Originally written by Dave Witten: Nov. 11, 2009
# Updated by Aditya Panchal: Oct. 9, 2018
# ===================================================================
from io import BytesIO
import pathlib
from PIL import Image
import dicom2jpg
import pydicom
import wx
import cv2
have_PIL = True
try:
import PIL.Image
except ImportError:
have_PIL = False
have_numpy = True
try:
import numpy as np
except ImportError:
have_numpy = False
# ----------------------------------------------------------------
# Initialize image capabilities.
# ----------------------------------------------------------------
wx.InitAllImageHandlers()
def MsgDlg(window, string, caption='OFAImage', style=wx.YES_NO | wx.CANCEL):
"""Common MessageDialog."""
dlg = wx.MessageDialog(window, string, caption, style)
result = dlg.ShowModal()
dlg.Destroy()
return result
class ViewerFrame(wx.Frame):
"""Class for main window."""
def __init__(self, parent, title, initial_file=None):
"""Create the pydicom image example's main frame window."""
style = wx.DEFAULT_FRAME_STYLE | wx.SUNKEN_BORDER | wx.CLIP_CHILDREN
wx.Frame.__init__(
self,
parent,
id=-1,
title="",
pos=wx.DefaultPosition,
size=wx.Size(1024, 768),
style=style)
# --------------------------------------------------------
# Set up the menubar.
# --------------------------------------------------------
self.mainmenu = wx.MenuBar()
# Make the 'File' menu.
menu = wx.Menu()
item = menu.Append(wx.ID_ANY, '&Open...\tCtrl+O', 'Open file for editing')
self.Bind(wx.EVT_MENU, self.OnFileOpen, item)
item = menu.Append(wx.ID_ANY, 'E&xit', 'Exit Program')
self.Bind(wx.EVT_MENU, self.OnFileExit, item)
self.mainmenu.Append(menu, '&File')
# Attach the menu bar to the window.
self.SetMenuBar(self.mainmenu)
# --------------------------------------------------------
# Set up the main splitter window.
# --------------------------------------------------------
self.mainSplitter = wx.SplitterWindow(self)
self.mainSplitter.SetMinimumPaneSize(1)
# -------------------------------------------------------------
# Create the folderTreeView on the left.
# -------------------------------------------------------------
dsstyle = wx.TR_LINES_AT_ROOT | wx.TR_HAS_BUTTONS
self.dsTreeView = wx.TreeCtrl(self.mainSplitter, style=dsstyle)
# --------------------------------------------------------
# Create the ImageView on the right pane.
# --------------------------------------------------------
imstyle = wx.VSCROLL | wx.HSCROLL | wx.CLIP_CHILDREN
self.imView = wx.Panel(self.mainSplitter, style=imstyle)
self.imView.Bind(wx.EVT_PAINT, self.OnPaint)
self.imView.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.imView.Bind(wx.EVT_SIZE, self.OnSize)
# --------------------------------------------------------
# Install the splitter panes.
# --------------------------------------------------------
self.mainSplitter.SplitVertically(self.dsTreeView, self.imView)
self.mainSplitter.SetSashPosition(300, True)
# --------------------------------------------------------
# Initialize some values
# --------------------------------------------------------
self.dcmdsRoot = False
self.foldersRoot = False
self.loadCentered = True
self.bitmap = None
self.Show(True)
print(initial_file)
if initial_file is not None:
path = pathlib.Path(initial_file)
self.show_file2(path.name, str(path))
def OnFileExit(self, event):
"""Exits the program."""
self.Destroy()
event.Skip()
def OnSize(self, event):
"""Window 'size' event."""
self.Refresh()
def OnEraseBackground(self, event):
"""Window 'erase background' event."""
pass
def populateTree(self, ds):
""" Populate the tree in the left window with the [desired]
dataset values"""
if not self.dcmdsRoot:
self.dcmdsRoot = self.dsTreeView.AddRoot(text="DICOM Objects")
else:
self.dsTreeView.DeleteChildren(self.dcmdsRoot)
self.recurse_tree(ds, self.dcmdsRoot)
self.dsTreeView.ExpandAll()
def recurse_tree(self, ds, parent, hide=False):
""" order the dicom tags """
for data_element in ds:
if isinstance(data_element.value, str):
text = str(data_element)
ip = self.dsTreeView.AppendItem(parent, text=text)
else:
ip = self.dsTreeView.AppendItem(parent, text=str(data_element))
if data_element.VR == "SQ":
for i, ds in enumerate(data_element.value):
item_describe = data_element.name.replace(" Sequence", "")
item_text = "%s %d" % (item_describe, i + 1)
rjust = item_text.rjust(128)
parentNodeID = self.dsTreeView.AppendItem(ip, text=rjust)
self.recurse_tree(ds, parentNodeID)
# --- Most of what is important happens below this line ---------------------
def OnFileOpen(self, event):
"""Opens a selected file."""
msg = 'Choose a file to add.'
dlg = wx.FileDialog(self, msg, '', '', '*.*', wx.FD_OPEN)
if dlg.ShowModal() == wx.ID_OK:
fullPath = dlg.GetPath()
imageFile = dlg.GetFilename()
# checkDICMHeader()
self.show_file2(imageFile, fullPath)
def OnPaint(self, event):
"""Window 'paint' event."""
dc = wx.PaintDC(self.imView)
dc = wx.BufferedDC(dc)
# paint a background just so it isn't *so* boring.
dc.SetBackground(wx.Brush("WHITE"))
dc.Clear()
dc.SetBrush(wx.Brush("GREY", wx.CROSSDIAG_HATCH))
windowsize = self.imView.GetSize()
dc.DrawRectangle(0, 0, windowsize[0], windowsize[1])
bmpX0 = 0
bmpY0 = 0
if self.bitmap is not None:
if self.loadCentered:
bmpX0 = (windowsize[0] - self.bitmap.Width) / 2
bmpY0 = (windowsize[1] - self.bitmap.Height) / 2
dc.DrawBitmap(self.bitmap, int(bmpX0), int(bmpY0), False)
# ------------------------------------------------------------
# ViewerFrame.ConvertWXToPIL()
# Expropriated from Andrea Gavana's
# ShapedButton.py in the wxPython dist
# ------------------------------------------------------------
def ConvertWXToPIL(self, bmp):
""" Convert wx.Image Into PIL Image. """
width = bmp.GetWidth()
height = bmp.GetHeight()
im = wx.EmptyImage(width, height)
im.fromarray("RGBA", (width, height), bmp.GetData())
return im
# ------------------------------------------------------------
# ViewerFrame.ConvertPILToWX()
# Expropriated from Andrea Gavana's
# ShapedButton.py in the wxPython dist
# ------------------------------------------------------------
def ConvertPILToWX(self, pil, alpha=True):
""" Convert PIL Image Into wx.Image. """
if alpha:
image = wx.Image(pil.size[0], pil.size[1], clear=True)
image.SetData(pil.convert("RGB").tobytes())
image.SetAlpha(pil.convert("RGBA").tobytes()[3::4])
else:
image = wx.Image(pil.size[0], pil.size[1], clear=True)
new_image = pil.convert('RGB')
data = new_image.tobytes()
image.SetData(data)
return image
def get_LUT_value(self, data, window, level):
"""Apply the RGB Look-Up Table for the given
data and window/level value."""
if not have_numpy:
raise ImportError("Numpy is not available. "
"See http://numpy.scipy.org/ "
"to download and install")
win = window
lvl = level
e = [
0, 255,
lambda data: ((data - (lvl - 0.5)) / (win - 1) + 0.5) * (255 - 0)
]
return np.piecewise(data, [
data <= (lvl - 0.5 - (win - 1) / 2),
data > (lvl - 0.5 + (win - 1) / 2)
], e)
# -----------------------------------------------------------
# ViewerFrame.loadPIL_LUT(dataset)
# Display an image using the Python Imaging Library (PIL)
# -----------------------------------------------------------
def loadPIL_LUT(self, dataset):
if not have_PIL:
raise ImportError("Python Imaging Library is not available."
" See http://www.pythonware.com/products/pil/"
" to download and install")
if 'PixelData' not in dataset:
raise TypeError("Cannot show image -- "
"DICOM dataset does not have pixel data")
# can only apply LUT if these values exist
if ('WindowWidth' not in dataset) or ('WindowCenter' not in dataset):
bits = dataset.BitsAllocated
samples = dataset.SamplesPerPixel
if bits == 8 and samples == 1:
mode = "L"
elif bits == 8 and samples == 3:
mode = "RGB"
# not sure about this -- PIL source says is
# 'experimental' and no documentation.
elif bits == 16:
# Also, should bytes swap depending
# on endian of file and system??
mode = "I;16"
else:
msg = "Don't know PIL mode for %d BitsAllocated" % (bits)
msg += " and %d SamplesPerPixel" % (samples)
raise TypeError(msg)
size = (dataset.Columns, dataset.Rows)
# Recommended to specify all details by
# http://www.pythonware.com/library/pil/handbook/image.htm
im = PIL.Image.frombuffer(mode, size, dataset.PixelData, "raw",
mode, 0, 1)
else:
ew = dataset['WindowWidth']
ec = dataset['WindowCenter']
ww = int(ew.value[0] if ew.VM > 1 else ew.value)
wc = int(ec.value[0] if ec.VM > 1 else ec.value)
image = self.get_LUT_value(dataset.pixel_array, ww, wc)
# Convert mode to L since LUT has only 256 values:
# http://www.pythonware.com/library/pil/handbook/image.htm
im = PIL.Image.fromarray(image).convert('L')
return im
def show_file(self, imageFile, fullPath):
""" Load the DICOM file, make sure it contains at least one
image, and set it up for display by OnPaint(). ** be
careful not to pass a unicode string to read_file or it will
give you 'fp object does not have a defer_size attribute,
or some such."""
ds = pydicom.dcmread(str(fullPath))
# change strings to unicode
ds.decode()
self.populateTree(ds)
if 'PixelData' in ds:
self.dImage = self.loadPIL_LUT(ds)
if self.dImage is not None:
tmpImage = self.ConvertPILToWX(self.dImage, False)
self.bitmap = wx.Bitmap(tmpImage)
self.Refresh()
def show_file2(self, imageFile, fullPath):
""" Load the DICOM file, make sure it contains at least one
image, and set it up for display by OnPaint(). ** be
careful not to pass a unicode string to read_file or it will
give you 'fp object does not have a defer_size attribute,
or some such."""
ds = pydicom.dcmread(str(fullPath))
# change strings to unicode
ds.decode()
self.populateTree(ds)
with open(fullPath, "rb") as f:
img_data = dicom2jpg.io2img(BytesIO(f.read()))
img = Image.fromarray(img_data)
#img.show()
tmpImage = self.ConvertPILToWX(img, False)
self.bitmap = wx.Bitmap(tmpImage)
self.Refresh()
# ------ This is just the initialization of the App ----
# =======================================================
# The main App Class.
# =======================================================
class BasicDicomViewer(wx.App):
"""Image Application."""
def __init__(self, initial_file=None, redirect=False, filename=None, useBestVisual=False, clearSigInt=True):
self.initial_file = initial_file
super().__init__(redirect=redirect, filename=filename, useBestVisual=useBestVisual, clearSigInt=clearSigInt)
def OnInit(self):
"""Create the Image Application."""
print(1)
ViewerFrame(None, 'Simple Dicom Viewer', initial_file=self.initial_file)
return True
# ------------------------------------------------------
# If this file is running as main or a
# standalone test, begin execution here.
# ------------------------------------------------------
if __name__ == '__main__':
app = BasicDicomViewer()
app.MainLoop()