├── debian ├── compat ├── prerm ├── preinst ├── postinst ├── postrm ├── control ├── copyright ├── rules └── changelog ├── virtual-dicom-printer.1 ├── .gitignore ├── QUtf8Settings ├── 99-virtual-dicom-printer.conf ├── .travis.yml ├── .ci └── git-install.sh ├── qutf8settings.h ├── virtual-dicom-printer.service ├── transcyrillic.h ├── appveyor.yml ├── product.h ├── virtual-dicom-printer.conf ├── virtual-dicom-printer.spec ├── virtual-dicom-printer.pro ├── storescp.h ├── buddy.yml ├── README.md ├── storescp.cpp ├── printscp.h ├── main.cpp ├── transcyrillic.cpp └── printscp.cpp /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /virtual-dicom-printer.1: -------------------------------------------------------------------------------- 1 | TBD -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | Makefile 3 | -------------------------------------------------------------------------------- /QUtf8Settings: -------------------------------------------------------------------------------- 1 | #include "qutf8settings.h" 2 | -------------------------------------------------------------------------------- /99-virtual-dicom-printer.conf: -------------------------------------------------------------------------------- 1 | if $programname == 'virtual-dicom-printer' then /var/log/virtual-dicom-printer.log 2 | if $programname == 'virtual-dicom-printer' then ~ 3 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$1" in 4 | remove) 5 | 6 | service virtual-dicom-printer stop || : 7 | ;; 8 | 9 | esac 10 | 11 | #DEBHELPER# 12 | -------------------------------------------------------------------------------- /debian/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$1" in 4 | install) 5 | 6 | useradd -rmb /var/lib -s /sbin/nologin virtprint || : 7 | ;; 8 | 9 | esac 10 | 11 | #DEBHELPER# 12 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$1" in 4 | configure) 5 | 6 | service rsyslog restart || : 7 | service virtual-dicom-printer start || : 8 | ;; 9 | 10 | esac 11 | 12 | #DEBHELPER# 13 | -------------------------------------------------------------------------------- /debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$1" in 4 | purge) 5 | 6 | # Remove all files 7 | rm -rf /var/lib/virtprint 8 | rm -f /var/log/virtual-dicom-printer.log 9 | 10 | userdel virtprint || : 11 | ;; 12 | 13 | esac 14 | 15 | #DEBHELPER# 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c++ 2 | sudo: required 3 | dist: trusty 4 | before_install: 5 | - sudo apt-get install -y debhelper qt5-default libdcmtk2-dev libqt5network5 libtesseract-dev libleptonica-dev fakeroot 6 | script: 7 | - dpkg-buildpackage -us -uc -I.git -I*.sh -rfakeroot 8 | 9 | 10 | -------------------------------------------------------------------------------- /.ci/git-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | URI=$1 3 | VER=$2 4 | OPT=$3 5 | PKG=`basename $URI .git` 6 | 7 | if [ -d $PKG ] 8 | then cd $PKG 9 | else git clone -q $URI -b $VER; cd $PKG; mkdir build 10 | fi 11 | 12 | cd build 13 | [ -f Makefile ] || cmake -Wno-dev .. $OPT 14 | 15 | cmake --build . --target install -- -j 4 16 | 17 | -------------------------------------------------------------------------------- /qutf8settings.h: -------------------------------------------------------------------------------- 1 | #ifndef QUTF8SETTINGS_H 2 | #define QUTF8SETTINGS_H 3 | 4 | #include 5 | #include 6 | 7 | class QUtf8Settings : public QSettings 8 | { 9 | Q_OBJECT 10 | public: 11 | explicit QUtf8Settings(QObject *parent = 0) 12 | : QSettings(parent) 13 | { 14 | setIniCodec("UTF-8"); 15 | } 16 | 17 | signals: 18 | 19 | public slots: 20 | }; 21 | 22 | #endif // QUTF8SETTINGS_H 23 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: virtual-dicom-printer 2 | Section: contrib/misc 3 | Priority: optional 4 | Maintainer: Softus team 5 | Standards-Version: 3.2.1 6 | Build-Depends: debhelper, qt5-default, libdcmtk2-dev, libxml2-dev, 7 | libqt5network5, libtesseract-dev, libleptonica-dev 8 | 9 | Package: virtual-dicom-printer 10 | Architecture: i386 amd64 11 | Pre-Depends: debconf (>= 1.1) | debconf-2.0 12 | Depends: ${shlibs:Depends} 13 | Description: Virtual printer for DICOM. 14 | Works as a proxy and spooler for a real printer(s). 15 | Also, all prints may be archived in a DICOM storage server. 16 | -------------------------------------------------------------------------------- /virtual-dicom-printer.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Virtual DICOM printer 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Environment=QT_LOGGING_TO_CONSOLE=1 8 | Environment=DCMDICTPATH=/usr/share/dcmtk/dicom.dic 9 | User=virtprint 10 | Group=virtprint 11 | WorkingDirectory=/var/lib/virtprint 12 | ExecStart=/usr/bin/virtual-dicom-printer 13 | Restart=always 14 | RestartSec=5 15 | StandardOutput=syslog 16 | StandardError=syslog 17 | SyslogIdentifier=virtual-dicom-printer 18 | 19 | # Give a reasonable amount of time for the server to start up/shut down 20 | TimeoutSec=300 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /transcyrillic.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #ifndef TRANSCYRILLIC_H 18 | #define TRANSCYRILLIC_H 19 | 20 | #include 21 | 22 | // Convert russian names written in latin symbols back to cyrillic 23 | // 24 | // IVANOV => ИВАНОВ 25 | // NEPOMNYASHCHIKH => НЕПОМНЯЩИХ 26 | // 27 | QString translateToCyrillic(const QString& str); 28 | QString translateToLatin(const QString& str); 29 | 30 | #endif // TRANSCYRILLIC_H 31 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | pull_requests: 3 | do_not_increment_build_number: true 4 | skip_non_tags: true 5 | image: Visual Studio 2013 6 | 7 | environment: 8 | QT_DIR: C:\Qt\5.7\mingw53_32 9 | MINGW_DIR: C:\Qt\Tools\mingw530_32 10 | PREFIX: C:\usr 11 | 12 | # Fix line endings on Windows 13 | init: 14 | - git config --global core.autocrlf true 15 | 16 | install: 17 | - mkdir %PREFIX% 18 | - mkdir %PREFIX%\bin && mkdir %PREFIX%\include && mkdir %PREFIX%\lib 19 | - set PATH=%MINGW_DIR%\bin;%QT_DIR%\bin;%PREFIX%\bin;c:\msys64\mingw32\bin;%PATH% 20 | 21 | # DCMTK 22 | - git clone git://git.dcmtk.org/dcmtk.git -b DCMTK-3.6.1_20150924 && cd dcmtk 23 | - mkdir build && cd build 24 | - set PATH=%PATH:C:\Program Files\Git\usr\bin=% 25 | - cmake -Wno-dev .. -DCMAKE_INSTALL_PREFIX=%PREFIX% -G "MinGW Makefiles" 26 | - cmake --build . --target install 27 | - set PATH=%PATH%;C:\Program Files\Git\usr\bin 28 | - cd ..\.. 29 | 30 | 31 | build_script: 32 | - qmake INCLUDEPATH+=%PREFIX%\include QMAKE_LIBDIR+=%PREFIX%\lib 33 | - mingw32-make 34 | 35 | artifacts: 36 | - path: Release\*.exe 37 | name: windows-executable 38 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: virtual-dicom-printer 3 | Source: http://softus.org/products/virtual-dicom-printer/packages 4 | 5 | Files: * 6 | Copyright: 2014-2016 Softus Inc. 7 | License: LGPL-2.1+ 8 | 9 | License: LGPL-2.1+ 10 | This package is free software; you can redistribute it and/or 11 | modify it under the terms of the GNU Lesser General Public 12 | License as published by the Free Software Foundation; either 13 | version 2.1 of the License, or (at your option) any later version. 14 | 15 | This package is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | Lesser General Public License for more details. 19 | 20 | You should have received a copy of the GNU Lesser General Public 21 | License along with this package; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | 24 | On Debian systems, the complete text of the GNU Lesser General 25 | Public License can be found in `/usr/share/common-licenses/LGPL'. 26 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | APPNAME := virtual-dicom-printer 3 | builddir: 4 | mkdir -p builddir 5 | 6 | builddir/Makefile: builddir 7 | cd builddir && qmake ../$(APPNAME).pro 8 | 9 | build: build-stamp 10 | 11 | build-stamp: builddir/Makefile 12 | dh_testdir 13 | # Add here commands to compile the package. 14 | cd builddir && $(MAKE) -j 2 15 | touch $@ 16 | 17 | clean: 18 | dh_testdir 19 | dh_testroot 20 | rm -f build-stamp 21 | # Add here commands to clean up after the build process. 22 | rm -rf builddir 23 | dh_clean 24 | install: build 25 | dh_testdir 26 | dh_testroot 27 | dh_clean -k 28 | dh_installdirs 29 | 30 | # Add here commands to install the package into debian/your_appname 31 | cd builddir && $(MAKE) INSTALL_ROOT=$(CURDIR)/debian/$(APPNAME) install 32 | # Build architecture-independent files here. 33 | binary-indep: build install 34 | # We have nothing to do by default. 35 | 36 | # Build architecture-dependent files here. 37 | binary-arch: build install 38 | dh_testdir 39 | dh_testroot 40 | dh_installdocs 41 | dh_installexamples 42 | dh_installman 43 | dh_link 44 | dh_strip 45 | dh_compress 46 | dh_fixperms 47 | dh_installdeb 48 | dh_shlibdeps 49 | dh_gencontrol 50 | dh_md5sums 51 | dh_builddeb 52 | 53 | binary: binary-indep binary-arch 54 | .PHONY: build clean binary-indep binary-arch binary install configure 55 | -------------------------------------------------------------------------------- /product.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #ifndef PRODUCT_H 18 | #define PRODUCT_H 19 | 20 | #define ORGANIZATION_FULL_NAME "Softus Inc." 21 | #define ORGANIZATION_DOMAIN "softus.org" 22 | 23 | #define PRODUCT_FULL_NAME "Virtual DICOM Printer" 24 | #define PRODUCT_SHORT_NAME "virtual-dicom-printer" // lowercase, no spaces 25 | 26 | #define PRODUCT_VERSION 0x010300 27 | #define PRODUCT_VERSION_STR "1.3" 28 | 29 | #define PRODUCT_SITE_URL "http://" ORGANIZATION_DOMAIN "/projects/" PRODUCT_SHORT_NAME "/" 30 | #define PRODUCT_NAMESPACE "org.softus." PRODUCT_SHORT_NAME 31 | 32 | #define SITE_UID_ROOT "1.2.643.2.66" 33 | 34 | #endif // PRODUCT_H 35 | -------------------------------------------------------------------------------- /virtual-dicom-printer.conf: -------------------------------------------------------------------------------- 1 | [General] 2 | storage-servers=SAMPLE_DICOM_SERVER 3 | debug-upstream= 4 | log-level= 5 | debug=0 6 | port=11112 7 | store-port= 8 | store-pdu-size=16384 9 | store-aetitle= 10 | timeout=30 11 | spool-interval-in-seconds=600 12 | spool-path=/var/spool/virtual-dicom-printer 13 | next-spool-ts= 14 | ocr-lang=eng 15 | block-mode=0 16 | bad-symbols="[^a-zA-Z0-9,:\\n ._\\-\\(\\)]" 17 | 18 | [query] 19 | url=http://krypton/krypton/dicom/rest/restricted/Image/saveHardcopyGrayscaleImage 20 | content-type=application/json 21 | ignore-errors=KRYPTON020301, KRYPTON020303, KRYPTON020304 22 | username=web 23 | password=web 24 | 25 | [tag] 26 | 1\key="0008,0060" 27 | 1\value=HC 28 | 2\key="0008,0016" 29 | 2\value=1.2.840.10008.5.1.1.29 30 | size=2 31 | 32 | [SAMPLE_DICOM_SERVER] 33 | address=pacs-server.local:11112 34 | aetitle=PACS_SERVER 35 | 36 | [SAMPLE_PRINTER] 37 | aetitle=KC_PLNK5_SCP 38 | upstream-address=10.0.0.31:6000 39 | upstream-aetitle=KC_PLNK5_SCP 40 | force-unique-series=0 41 | force-unique-study=0 42 | debug-upstream=0 43 | info\1\key="0008,0070" 44 | info\1\value=KONICA MINOLTA 45 | info\2\key="0008,1090" 46 | info\2\value=Printlink5-IN 47 | info\3\key="0018,1000" 48 | info\3\value=1234 49 | info\4\key="0018,1020" 50 | info\4\value=V2.00R06 51 | info\5\key="2110,0030" 52 | info\5\value=DRYPRO832 53 | info\size=5 54 | tag\1\key="0010,0010" 55 | tag\1\pattern=[^\n]+ 56 | tag\1\query-parameter=patientFullName 57 | tag\1\rect=@Rect(-300 0 300 70) 58 | tag\2\key="0010,0020" 59 | tag\2\query-parameter=medicalRecordNumber 60 | tag\2\value=843967 61 | tag\3\key="0010,0030" 62 | tag\4\key="0010,0040" 63 | tag\5\query-parameter=roomCode 64 | tag\5\value=130 65 | tag\size=5 66 | query\ignore-errors=KRYPTON020305 67 | -------------------------------------------------------------------------------- /virtual-dicom-printer.spec: -------------------------------------------------------------------------------- 1 | Name: virtual-dicom-printer 2 | Provides: virtual-dicom-printer 3 | Version: 1.3 4 | Release: 1%{?dist} 5 | License: LGPL-2.1+ 6 | Source: %{name}.tar.gz 7 | URL: http://softus.org/products/virtual-dicom-printer 8 | Vendor: Softus Inc. 9 | Packager: Softus Inc. 10 | Summary: Virtual printer for DICOM. 11 | 12 | %description 13 | Virtual printer for DICOM. 14 | 15 | Works as a proxy and spooler for a real printer(s). 16 | Also, all prints may be archived in a DICOM storage server. 17 | 18 | %global debug_package %{nil} 19 | 20 | Requires: dcmtk, redhat-lsb-core 21 | BuildRequires: make, gcc-c++, systemd 22 | 23 | %{?rhl:BuildRequires: qt5-qtbase-devel, dcmtk-devel, openssl-devel, tesseract-devel, libxml2-devel} 24 | 25 | %{?fedora:BuildRequires: qt-devel, dcmtk-devel, openssl-devel, tesseract-devel, libxml2-devel} 26 | 27 | %{?suse_version:BuildRequires: libqt5-qtbase-devel, dcmtk-devel, openssl-devel, tesseract-ocr-devel, libxml2-devel} 28 | 29 | %if 0%{?mageia} 30 | %define qmake qmake 31 | BuildRequires: qttools5 32 | %ifarch x86_64 amd64 33 | BuildRequires: lib64qt5base5-devel, lib64tesseract-devel 34 | %else 35 | BuildRequires: libqt5base5-devel, libtesseract-devel 36 | %endif 37 | %else 38 | %define qmake qmake-qt5 39 | %endif 40 | 41 | %prep 42 | %setup -c %{name} 43 | 44 | %build 45 | %{qmake} PREFIX=%{_prefix} QMAKE_CFLAGS+="%optflags" QMAKE_CXXFLAGS+="%optflags"; 46 | make %{?_smp_mflags}; 47 | 48 | %install 49 | make install INSTALL_ROOT="%buildroot"; 50 | 51 | %files 52 | %defattr(-,root,root) 53 | %config(noreplace) %{_sysconfdir}/xdg/softus.org/%{name}.conf 54 | %config(noreplace) %{_sysconfdir}/rsyslog.d/99-%{name}.conf 55 | %{_mandir}/man1/%{name}.1.* 56 | %{_bindir}/%{name} 57 | %{_unitdir}/%{name}.service 58 | 59 | %pre 60 | /usr/sbin/groupadd -r virtprint || : 61 | /usr/sbin/useradd -rmb /var/lib -s /sbin/nologin -g virtprint virtprint || : 62 | chown virtprint:virtprint /var/lib/virtprint 63 | chmod 775 /var/lib/virtprint 64 | 65 | %post 66 | systemctl enable %{name} 67 | service %{name} start || : 68 | 69 | %preun 70 | service %{name} stop || : 71 | 72 | %postun 73 | /usr/sbin/userdel virtprint || : 74 | /usr/sbin/groupdel virtprint || : 75 | 76 | %changelog 77 | * Mon Mar 6 2017 Pavel Bludov 78 | + Version 1.2 79 | - Change centos dependencies 80 | -------------------------------------------------------------------------------- /virtual-dicom-printer.pro: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2013-2018 Softus Inc. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU Lesser General Public License as published by 5 | # the Free Software Foundation; version 2. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU Lesser General Public License 13 | # along with this program. If not, see . 14 | 15 | lessThan(QT_MAJOR_VERSION, 5): error (QT 5.0 or newer is required) 16 | 17 | isEmpty(PREFIX): PREFIX = /usr 18 | DEFINES += PREFIX=$$PREFIX 19 | CONFIG += link_pkgconfig c++11 20 | QT += network 21 | QT -= gui 22 | LIBS += -ldcmpstat -ldcmnet -ldcmdata -ldcmimgle -ldcmdsig -ldcmsr -ldcmtls -ldcmqrdb -lxml2 -loflog -lofstd -lz 23 | unix:LIBS += -lssl 24 | win32:LIBS += -lws2_32 -ladvapi32 -lnetapi32 25 | 26 | OPTIONAL_LIBS = lept tesseract 27 | for (mod, OPTIONAL_LIBS) { 28 | modVer = $$system(pkg-config --silence-errors --modversion $$mod) 29 | isEmpty(modVer) { 30 | message("Optional package $$mod not installed") 31 | } else { 32 | message("Found $$mod version $$modVer") 33 | PKGCONFIG += $$mod 34 | DEFINES += WITH_$$upper($$replace(mod, \W, _)) 35 | } 36 | } 37 | 38 | TARGET = virtual-dicom-printer 39 | CONFIG += console 40 | CONFIG -= app_bundle 41 | 42 | TEMPLATE = app 43 | SOURCES += main.cpp \ 44 | printscp.cpp \ 45 | storescp.cpp \ 46 | transcyrillic.cpp 47 | 48 | HEADERS += \ 49 | printscp.h \ 50 | product.h \ 51 | storescp.h \ 52 | transcyrillic.h \ 53 | qutf8settings.h \ 54 | QUtf8Settings 55 | 56 | target.path=$$PREFIX/bin 57 | man.files=virtual-dicom-printer.1 58 | man.path=$$PREFIX/share/man/man1 59 | cfg.files=virtual-dicom-printer.conf 60 | cfg.path=/etc/xdg/softus.org 61 | syslog.files=99-virtual-dicom-printer.conf 62 | syslog.path=/etc/rsyslog.d 63 | systemd.files=virtual-dicom-printer.service 64 | equals(OS_DISTRO, debian) | equals(OS_DISTRO, ubuntu) { 65 | systemd.path=/lib/systemd/system 66 | } else { 67 | systemd.path=/usr/lib/systemd/system 68 | } 69 | 70 | INSTALLS += target man cfg syslog systemd 71 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | virtual-dicom-printer (1.3.0-2xenial1) xenial; urgency=medium 2 | 3 | * Enforce UTF-8 in settings file. 4 | 5 | -- Pavel Bludov Tue, 23 Jan 2018 14:42:41 +0800 6 | 7 | virtual-dicom-printer (1.3.0-1xenial1) xenial; urgency=medium 8 | 9 | * /etc/sysconfig/virtual-dicom-printer file. 10 | 11 | -- Pavel Bludov Tue, 23 Jan 2018 14:42:41 +0800 12 | 13 | virtual-dicom-printer (1.2.1-1xenial1) xenial; urgency=medium 14 | 15 | * RHEL builds. 16 | * Sample config file /etc/xdg/softus.org/virtual-dicom-printer.conf 17 | 18 | -- Pavel Bludov Thu, 9 Mar 2017 10:51:21 +0800 19 | 20 | virtual-dicom-printer (1.2.0-1xenial1) xenial; urgency=medium 21 | 22 | * Copyright update. 23 | 24 | -- Pavel Bludov Thu, 24 Nov 2016 11:44:15 +0800 25 | 26 | virtual-dicom-printer (1.2.0) debian; 27 | 28 | * Resend failed prints in a worker process too 29 | 30 | -- Pavel Bludov Fri, 25 Mar 2016 12:41:33 +0800 31 | 32 | virtual-dicom-printer (1.1.0) debian; 33 | 34 | * Move all the work to child process. 35 | * Print ip in case of aborted connection. 36 | * fix spool folder. 37 | * Remove non printble symbols from OCR text. 38 | 39 | -- Pavel Bludov Wed, 23 Mar 2016 12:34:40 +0800 40 | 41 | virtual-dicom-printer (1.0.9) debian; 42 | 43 | * Resend failed prints every 10 min regardless of client connections. 44 | 45 | -- Pavel Bludov Wed, 16 Dec 2015 13:54:24 +0800 46 | 47 | 48 | virtual-dicom-printer (1.0.9) debian; 49 | 50 | * Load default values for "debug-upstream" & "force-unique-*" parameters 51 | * from global config section. 52 | 53 | -- Pavel Bludov Thu, 9 Apr 2015 13:20:09 +0800 54 | 55 | 56 | virtual-dicom-printer (1.0.8) debian; 57 | 58 | * Trying to avoid deadlock on child process exit. 59 | * Force all required fields be set to something. 60 | * More verbose output on exit. 61 | 62 | -- Pavel Bludov Tue, 9 Dec 2014 13:57:01 +0800 63 | 64 | virtual-dicom-printer (1.0.7) debian; 65 | 66 | * Workaround for EADDRINUSE after crash. 67 | * Mistype fix & assoc->params check. 68 | * Readable log files. 69 | * Use SOPInstanceUID as file name instead of PatientName_PatientID. 70 | * Mistype: 'hhmmss' is 6 symbols long, not 8. 71 | * isaString check moved to the right place. 72 | * strerr wrapped with QString::fromLocal8Bit, both 'yyyymmdd' and 'yyyy- 73 | * 'hhmmss' for time values returned from the web service. 74 | * Save attributes from web service to disk for failed store servers. 75 | * Spool print queries, if web service/storage servers failed to process. 76 | 77 | -- Pavel Bludov Thu, 23 Oct 2014 13:03:15 +0900 78 | 79 | virtual-dicom-printer (1.0.6) debian; 80 | 81 | * Force unique series for every instance (spooler optimization workaround). 82 | 83 | -- Pavel Bludov Tue, 9 Sep 2014 14:54:14 +0900 84 | 85 | virtual-dicom-printer (1.0.0) debian; 86 | 87 | * Initial version. 88 | 89 | -- Pavel Bludov Mon, 14 Jul 2014 15:01:19 +0900 90 | -------------------------------------------------------------------------------- /storescp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #ifndef STORESCP_H 18 | #define STORESCP_H 19 | 20 | #include 21 | 22 | #ifdef UNICODE 23 | #define DCMTK_UNICODE_BUG_WORKAROUND 24 | #undef UNICODE 25 | #endif 26 | 27 | #define HAVE_CONFIG_H 28 | #include /* make sure OS specific configuration is included first */ 29 | #include /* for enum types */ 30 | #include 31 | 32 | #ifdef DCMTK_UNICODE_BUG_WORKAROUND 33 | #define UNICODE 34 | #undef DCMTK_UNICODE_BUG_WORKAROUND 35 | #endif 36 | 37 | class DicomImage; 38 | struct T_ASC_Association; 39 | 40 | class StoreSCP : public QObject 41 | { 42 | Q_OBJECT 43 | 44 | public: 45 | explicit StoreSCP(const QString& server, QObject *parent = 0); 46 | ~StoreSCP(); 47 | 48 | /** transfers the dataset to the storage server. 49 | * @param dataset to send 50 | * @param sopInstance unique identifier of the dataset 51 | * @return result indicating whether transfer was successful 52 | */ 53 | OFCondition sendToServer(DcmDataset* dataset, const char* sopInstance); 54 | 55 | private: 56 | /** prepares connection parameters for the Store SCP. 57 | * @param peerAet called AETITLE of the server 58 | * @param peerAddress network address of the server 59 | * @param timeout timeout for network operations, in seconds 60 | * @param abstractSyntax SOP class from the dataset 61 | * @param transferSyntax transfer syntax from the dataset 62 | * @return result indicating whether association negotiation was successful, 63 | * unsuccessful or whether termination of the server was requested. 64 | */ 65 | T_ASC_Parameters* initAssocParams(const QString& peerAet, const QString& peerAddress, int timeout, 66 | const char *abstractSyntax, const char* transferSyntax); 67 | 68 | /** transfers the dataset to the storage server. 69 | * @param dataset to send 70 | * @param abstractSyntax SOP class from the dataset 71 | * @param sopInstance unique identifier of the dataset 72 | * @return result indicating whether transfer was successful 73 | */ 74 | OFCondition cStoreRQ(DcmDataset* dataset, const char *abstractSyntax, const char* sopInstance); 75 | 76 | /** destroys the association managed by this object. 77 | */ 78 | void dropAssociation(); 79 | 80 | // Our section in the configuration file 81 | // 82 | QString server; 83 | 84 | // blocking mode for receive 85 | // 86 | T_DIMSE_BlockingMode blockMode; 87 | 88 | // timeout for receive 89 | // 90 | int timeout; 91 | 92 | // the DICOM network and listen port 93 | // 94 | T_ASC_Network *net; 95 | 96 | // the network association over which the print SCP is operating 97 | // 98 | T_ASC_Association *assoc; 99 | 100 | // Transfer context (usually LittleEndianExplicit) 101 | // 102 | T_ASC_PresentationContextID presId; 103 | }; 104 | 105 | #endif // STORESCP_H 106 | -------------------------------------------------------------------------------- /buddy.yml: -------------------------------------------------------------------------------- 1 | - pipeline: "Buddy" 2 | trigger_mode: "ON_EVERY_PUSH" 3 | ref_name: "master" 4 | actions: 5 | - action: "CentOS" 6 | type: "BUILD" 7 | docker_image_name: "library/centos" 8 | docker_image_tag: "latest" 9 | cached_dirs: 10 | - "/cache-centos" 11 | execute_commands: 12 | - if [ ! -d cache-centos ]; then mkdir cache-centos; fi 13 | - cd cache-centos 14 | - ../.ci/git-install.sh https://github.com/DCMTK/dcmtk.git DCMTK-3.6.3 "-DCMAKE_INSTALL_PREFIX=/usr -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_WRAP=OFF -DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_ICONV=OFF" 15 | - cd .. 16 | - export RPM_BUILD_NCPUS=2 17 | - tar czf ../${project.name}.tar.gz --exclude=cache* --exclude=debian --exclude=*.yml * && rpmbuild -ta ../${project.name}.tar.gz 18 | setup_commands: 19 | - yum install -y epel-release 20 | - yum update -y 21 | - yum install -y redhat-lsb rpm-build git make cmake gcc-c++ qt5-qtbase-devel tesseract-devel openssl-devel libxml2-devel 22 | trigger_condition: "ALWAYS" 23 | working_directory: "/buddy/centos" 24 | 25 | - action: "Debian" 26 | type: "BUILD" 27 | docker_image_name: "library/debian" 28 | docker_image_tag: "latest" 29 | execute_commands: 30 | - dpkg-buildpackage -us -uc -tc -I*.yml -Icache* -rfakeroot 31 | setup_commands: 32 | - apt update -q 33 | - apt upgrade -y 34 | - apt install -y lsb-release debhelper fakeroot libdcmtk2-dev qt5-default libleptonica-dev libtesseract-dev 35 | trigger_condition: "ALWAYS" 36 | working_directory: "/buddy/debian" 37 | 38 | - action: "Fedora" 39 | type: "BUILD" 40 | docker_image_name: "library/fedora" 41 | docker_image_tag: "latest" 42 | execute_commands: 43 | - export RPM_BUILD_NCPUS=2 44 | - tar czf ../${project.name}.tar.gz --exclude=cache* --exclude=debian --exclude=*.yml * && rpmbuild -ta ../${project.name}.tar.gz 45 | setup_commands: 46 | - dnf install -y redhat-lsb rpm-build make gcc-c++ qt5-qtbase-devel dcmtk-devel tesseract-devel openssl-devel libxml2-devel 47 | trigger_condition: "ALWAYS" 48 | working_directory: "/buddy/fedora" 49 | 50 | - action: "openSUSE" 51 | type: "BUILD" 52 | docker_image_name: "library/opensuse" 53 | docker_image_tag: "latest" 54 | execute_commands: 55 | - export RPM_BUILD_NCPUS=2 56 | - tar czf ../${project.name}.tar.gz --exclude=cache* --exclude=debian --exclude=*.yml * && rpmbuild -ta ../${project.name}.tar.gz 57 | setup_commands: 58 | - zypper install -y lsb-release rpm-build make libqt5-qtbase-devel dcmtk-devel tesseract-ocr-devel openssl-devel libxml2-devel 59 | trigger_condition: "ALWAYS" 60 | working_directory: "/buddy/opensuse" 61 | 62 | - action: "Mageia" 63 | type: "BUILD" 64 | docker_image_name: "library/mageia" 65 | docker_image_tag: "latest" 66 | cached_dirs: 67 | - "/cache-mageia" 68 | execute_commands: 69 | - if [ ! -d cache-mageia ]; then mkdir cache-mageia; fi 70 | - cd cache-mageia 71 | - ../.ci/git-install.sh https://github.com/DCMTK/dcmtk.git DCMTK-3.6.3 "-DCMAKE_INSTALL_PREFIX=/usr -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_WRAP=OFF -DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_ICONV=OFF" 72 | - cd .. 73 | - export RPM_BUILD_NCPUS=2 74 | - tar czf ../${project.name}.tar.gz --exclude=cache* --exclude=debian --exclude=*.yml * && rpmbuild -ta ../${project.name}.tar.gz 75 | setup_commands: 76 | - dnf install -y lsb-release rpm-build git make cmake gcc-c++ qttools5 lib64qt5base5-devel lib64tesseract-devel 77 | trigger_condition: "ALWAYS" 78 | working_directory: "/buddy/mageia" 79 | 80 | - action: "Ubuntu" 81 | type: "BUILD" 82 | docker_image_name: "library/ubuntu" 83 | docker_image_tag: "latest" 84 | execute_commands: 85 | - dpkg-buildpackage -us -uc -tc -I*.yml -Icache* -rfakeroot 86 | setup_commands: 87 | - apt update -q 88 | - apt upgrade -y 89 | - apt install -y lsb-release debhelper fakeroot libdcmtk2-dev qt5-default libleptonica-dev libtesseract-dev 90 | trigger_condition: "ALWAYS" 91 | working_directory: "/buddy/ubuntu" 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | virtual-dicom-printer 2 | ========= 3 | 4 | [![Buddy pipeline](https://app.buddy.works/pbludov/virtual-dicom-printer/pipelines/pipeline/129387/badge.svg?token=bf26fe8fed990190f11227bb2aa0c7d1e71118737795eed7b5069fff7106a015)](https://app.buddy.works/pbludov/virtual-dicom-printer/pipelines/pipeline/129387) 5 | [![Build Status](https://api.travis-ci.org/Softus/virtual-dicom-printer.svg?branch=master)](https://travis-ci.org/Softus/virtual-dicom-printer) 6 | [![Build status](https://ci.appveyor.com/api/projects/status/82ofqvp1710uwq3o?svg=true)](https://ci.appveyor.com/project/pbludov/virtual-dicom-printer) 7 | 8 | Introduction 9 | ============ 10 | 11 | Virtual printer for DICOM. 12 | Works as a proxy and spooler for a real printer(s). 13 | Also, all prints may be archived in a DICOM storage server. 14 | 15 | Requirements 16 | ============ 17 | 18 | * [Qt](http://qt-project.org/) 5.0.2 or higher; 19 | * [DCMTK](http://dcmtk.org/) 3.6.0 or higher; 20 | 21 | Installation 22 | ============ 23 | 24 | Debian/Ubuntu/Mint 25 | ------------------ 26 | 27 | 1. Install build dependecies 28 | 29 | sudo apt install lsb-release debhelper fakeroot libdcmtk2-dev \ 30 | qt5-default libtesseract-dev 31 | 32 | 2. Make 33 | 34 | qmake virtual-dicom-printer.pro 35 | make 36 | 37 | 3. Install 38 | 39 | sudo make install 40 | 41 | 4. Create Package 42 | 43 | dpkg-buildpackage -us -uc -tc -I*.yml -Icache* -rfakeroot 44 | 45 | CentOS 46 | ------ 47 | 48 | 1. Install build dependecies 49 | 50 | sudo yum install -y redhat-lsb rpm-build git make cmake gcc-c++ \ 51 | qt5-qtbase-devel tesseract-devel openssl-devel libxml2-devel git 52 | 53 | 2. Build DCMTK 54 | 55 | .ci/git-install.sh https://github.com/DCMTK/dcmtk.git DCMTK-3.6.3 \ 56 | "-DCMAKE_INSTALL_PREFIX=/usr -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_WRAP=OFF -DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_ICONV=OFF" 57 | 3. Make 58 | 59 | qmake-qt5 virtual-dicom-printer.pro 60 | make 61 | 62 | 4. Install 63 | 64 | sudo make install 65 | 66 | 5. Create Package 67 | 68 | tar czf ../virtual-dicom-printer.tar.gz --exclude=cache* --exclude=debian \ 69 | --exclude=*.yml * && rpmbuild -ta ../virtual-dicom-printer.tar.gz 70 | 71 | Fedora 72 | ------ 73 | 74 | 1. Install build dependecies 75 | 76 | sudo dnf install redhat-lsb rpm-build make gcc-c++ qt5-qtbase-devel \ 77 | dcmtk-devel tesseract-devel openssl-devel libxml2-devel 78 | 79 | 2. Make 80 | 81 | qmake-qt5 virtual-dicom-printer.pro 82 | make 83 | 84 | 3. Install 85 | 86 | sudo make install 87 | 88 | 4. Create Package 89 | 90 | tar czf /tmp/virtual-dicom-printer.tar.gz * --exclude=.git && rpmbuild -ta /tmp/virtual-dicom-printer.tar.gz 91 | 92 | Mageia 93 | ------ 94 | 95 | 1. Install build dependecies 96 | 97 | sudo dnf install lsb-release rpm-build git make cmake gcc-c++ \ 98 | qttools5 lib64qt5base5-devel lib64tesseract-devel git 99 | 100 | 2. Build DCMTK 101 | 102 | .ci/git-install.sh https://github.com/DCMTK/dcmtk.git DCMTK-3.6.3 \ 103 | "-DCMAKE_INSTALL_PREFIX=/usr -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_WRAP=OFF -DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_ICONV=OFF" 104 | 3. Make 105 | 106 | qmake virtual-dicom-printer.pro 107 | make 108 | 109 | 4. Install 110 | 111 | sudo make install 112 | 113 | 5. Create Package 114 | 115 | tar czf ../virtual-dicom-printer.tar.gz --exclude=cache* --exclude=debian \ 116 | --exclude=*.yml * && rpmbuild -ta ../virtual-dicom-printer.tar.gz 117 | 118 | openSUSE 119 | -------- 120 | 121 | 1. Install build dependecies 122 | 123 | sudo zypper install lsb-release rpm-build make libqt5-qtbase-devel \ 124 | dcmtk-devel tesseract-ocr-devel openssl-devel libxml2-devel 125 | 126 | 2. Make 127 | 128 | qmake-qt5 virtual-dicom-printer.pro 129 | make 130 | 131 | 3. Install 132 | 133 | sudo make install 134 | 135 | 4. Create Package 136 | 137 | tar czf /tmp/virtual-dicom-printer.tar.gz * --exclude=.git && rpmbuild -ta /tmp/virtual-dicom-printer.tar.gz 138 | 139 | Windows (Visual Studio) 140 | ----------------------- 141 | 142 | 1. Install build dependecies 143 | 144 | * [CMake](https://cmake.org/download/) 145 | * [pkg-config](http://ftp.gnome.org/pub/gnome/binaries/win32/dependencies/) 146 | * [Qt 5.5 MSVC](https://download.qt.io/archive/qt/5.5/) 147 | * [DCMTK](http://dcmtk.org/dcmtk.php.en) 148 | 149 | 2. Build 3-rd party libraries 150 | 151 | # DCMTK 152 | cd dcmtk 153 | mkdir build && cd build 154 | cmake -Wno-dev .. -DCMAKE_INSTALL_PREFIX=c:\usr -G "Visual Studio " \ 155 | -DDCMTK_WITH_OPENSSL=OFF -DDCMTK_WITH_ICU=OFF -DDCMTK_WITH_ICONV=OFF 156 | cmake --build . --target install 157 | 158 | 3. Make 159 | 160 | qmake-qt5 161 | nmake -f Makefile.Release 162 | -------------------------------------------------------------------------------- /storescp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #include 18 | #include 19 | #include "QUtf8Settings" 20 | 21 | #ifdef UNICODE 22 | #define DCMTK_UNICODE_BUG_WORKAROUND 23 | #undef UNICODE 24 | #endif 25 | 26 | #include "storescp.h" 27 | #include 28 | 29 | #ifdef DCMTK_UNICODE_BUG_WORKAROUND 30 | #define UNICODE 31 | #undef DCMTK_UNICODE_BUG_WORKAROUND 32 | #endif 33 | 34 | StoreSCP::StoreSCP(const QString& server, QObject *parent) 35 | : QObject(parent) 36 | , server(server) 37 | , blockMode(DIMSE_BLOCKING) 38 | , timeout(0) 39 | , net(nullptr) 40 | , assoc(nullptr) 41 | { 42 | QUtf8Settings settings; 43 | blockMode = (T_DIMSE_BlockingMode)settings.value("block-mode", blockMode).toInt(); 44 | timeout = settings.value("timeout", timeout).toInt(); 45 | } 46 | 47 | StoreSCP::~StoreSCP() 48 | { 49 | dropAssociation(); 50 | ASC_dropNetwork(&net); 51 | } 52 | 53 | void StoreSCP::dropAssociation() 54 | { 55 | if (assoc) 56 | { 57 | ASC_dropSCPAssociation(assoc); 58 | ASC_destroyAssociation(&assoc); 59 | assoc = nullptr; 60 | } 61 | } 62 | 63 | T_ASC_Parameters* StoreSCP::initAssocParams(const QString& peerAet, const QString& peerAddress, int timeout, 64 | const char* abstractSyntax, const char* transferSyntax) 65 | { 66 | QUtf8Settings settings; 67 | 68 | DIC_NODENAME localHost; 69 | T_ASC_Parameters* params = nullptr; 70 | 71 | auto cond = ASC_initializeNetwork(NET_REQUESTOR, settings.value("store-port").toInt(), timeout, &net); 72 | if (cond.good()) 73 | { 74 | cond = ASC_createAssociationParameters(¶ms, settings.value("store-pdu-size", ASC_DEFAULTMAXPDU).toInt()); 75 | if (cond.good()) 76 | { 77 | auto appAet = settings.value("store-aetitle", qApp->applicationName()).toString().toUpper().toUtf8(); 78 | ASC_setAPTitles(params, appAet, peerAet.toUtf8(), nullptr); 79 | 80 | /* Figure out the presentation addresses and copy the */ 81 | /* corresponding values into the DcmAssoc parameters.*/ 82 | gethostname(localHost, sizeof(localHost) - 1); 83 | ASC_setPresentationAddresses(params, localHost, peerAddress.toUtf8()); 84 | 85 | if (transferSyntax) 86 | { 87 | const char* arr[] = { transferSyntax }; 88 | cond = ASC_addPresentationContext(params, 1, abstractSyntax, arr, 1); 89 | } 90 | else 91 | { 92 | /* Set the presentation contexts which will be negotiated */ 93 | /* when the network connection will be established */ 94 | const char* transferSyntaxes[] = 95 | { 96 | #if __BYTE_ORDER == __LITTLE_ENDIAN 97 | UID_LittleEndianExplicitTransferSyntax, UID_BigEndianExplicitTransferSyntax, 98 | #elif __BYTE_ORDER == __BIG_ENDIAN 99 | UID_BigEndianExplicitTransferSyntax, UID_LittleEndianExplicitTransferSyntax, 100 | #else 101 | #error "Unsupported byte order" 102 | #endif 103 | UID_LittleEndianImplicitTransferSyntax 104 | }; 105 | 106 | cond = ASC_addPresentationContext(params, 1, abstractSyntax, 107 | transferSyntaxes, sizeof(transferSyntaxes)/sizeof(transferSyntaxes[0])); 108 | } 109 | 110 | if (cond.good()) 111 | { 112 | return params; 113 | } 114 | 115 | ASC_destroyAssociationParameters(¶ms); 116 | } 117 | } 118 | 119 | qDebug() << QString::fromLocal8Bit(cond.text()); 120 | return nullptr; 121 | } 122 | 123 | OFCondition StoreSCP::cStoreRQ(DcmDataset* dataset, const char* abstractSyntax, const char* sopInstance) 124 | { 125 | T_DIMSE_C_StoreRQ req; 126 | T_DIMSE_C_StoreRSP rsp; 127 | bzero((char*)&req, sizeof(req)); 128 | bzero((char*)&rsp, sizeof(rsp)); 129 | DcmDataset *statusDetail = nullptr; 130 | 131 | /* prepare the transmission of data */ 132 | req.MessageID = assoc->nextMsgID++; 133 | strcpy(req.AffectedSOPClassUID, abstractSyntax); 134 | strcpy(req.AffectedSOPInstanceUID, sopInstance); 135 | req.DataSetType = DIMSE_DATASET_PRESENT; 136 | req.Priority = DIMSE_PRIORITY_LOW; 137 | 138 | /* finally conduct transmission of data */ 139 | auto cond = DIMSE_storeUser(assoc, presId, &req, nullptr, dataset, nullptr, nullptr, 140 | 0 == timeout? DIMSE_BLOCKING: DIMSE_NONBLOCKING, timeout, &rsp, &statusDetail); 141 | 142 | if (rsp.DimseStatus) 143 | { 144 | OFString err; 145 | if (!statusDetail || statusDetail->findAndGetOFString(DCM_ErrorComment, err).bad() || err.length() == 0) 146 | { 147 | err.assign(QString::number(rsp.DimseStatus).toUtf8()); 148 | } 149 | 150 | cond = makeOFCondition(0, rsp.DimseStatus, OF_error, err.c_str()); 151 | } 152 | 153 | delete statusDetail; 154 | return cond; 155 | } 156 | 157 | OFCondition StoreSCP::sendToServer(DcmDataset* rqDataset, const char *sopInstance) 158 | { 159 | DcmXfer filexfer(rqDataset->getOriginalXfer()); 160 | auto xfer = filexfer.getXferID(); 161 | OFString sopClass; 162 | rqDataset->findAndGetOFString(DCM_SOPClassUID, sopClass); 163 | 164 | QUtf8Settings settings; 165 | settings.beginGroup(server); 166 | auto timeout = settings.value("timeout").toInt(); 167 | T_ASC_Parameters* params = initAssocParams(settings.value("aetitle").toString(), 168 | settings.value("address").toString(), 169 | timeout, sopClass.c_str(), xfer); 170 | 171 | auto cond = ASC_requestAssociation(net, params, &assoc); 172 | if (cond.bad()) 173 | { 174 | qDebug() << "Failed to create association to" << server; 175 | } 176 | else 177 | { 178 | // Dump general information concerning the establishment of the network connection if required 179 | // 180 | qDebug() << "DcmAssoc to" << server << "accepted (max send PDV: " << assoc->sendPDVLength << ")"; 181 | 182 | // Figure out which of the accepted presentation contexts should be used 183 | // 184 | presId = ASC_findAcceptedPresentationContextID(assoc, sopClass.c_str(), xfer); 185 | if (presId != 0) 186 | { 187 | cond = cStoreRQ(rqDataset, sopClass.c_str(), sopInstance); 188 | } 189 | else 190 | { 191 | cond = makeOFCondition(0, 1, OF_error, "Presentation context id not found"); 192 | } 193 | 194 | ASC_releaseAssociation(assoc); 195 | ASC_destroyAssociation(&assoc); 196 | assoc = nullptr; 197 | } 198 | settings.endGroup(); 199 | 200 | if (cond.bad()) 201 | { 202 | qDebug() << "Failed to store " << sopInstance << QString::fromLocal8Bit(cond.text()); 203 | } 204 | 205 | return cond; 206 | } 207 | 208 | -------------------------------------------------------------------------------- /printscp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #ifndef PRINTSCP_H 18 | #define PRINTSCP_H 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #ifdef WITH_TESSERACT 25 | #include 26 | #endif 27 | 28 | #define DEFAULT_LISTEN_PORT 10005 29 | #define DEFAULT_TIMEOUT 30 30 | #define DEFAULT_OCR_LANG "eng" 31 | #define DEFAULT_CONTENT_TYPE "application/xml" 32 | #define DEFAULT_CHARSET "UTF-8" 33 | 34 | #ifdef UNICODE 35 | #define DCMTK_UNICODE_BUG_WORKAROUND 36 | #undef UNICODE 37 | #endif 38 | 39 | #define HAVE_CONFIG_H 40 | #include // make sure OS specific configuration is included first 41 | #include // for enum types 42 | #include 43 | 44 | #ifdef DCMTK_UNICODE_BUG_WORKAROUND 45 | #define UNICODE 46 | #undef DCMTK_UNICODE_BUG_WORKAROUND 47 | #endif 48 | 49 | class DicomImage; 50 | struct T_ASC_Association; 51 | 52 | class PrintSCP : public QObject 53 | { 54 | Q_OBJECT 55 | 56 | public: 57 | PrintSCP(T_ASC_Association *assoc, QObject *parent = 0, const QString& printer = QString()); 58 | ~PrintSCP(); 59 | 60 | /** performs association negotiation for the Print SCP. Depending on the 61 | * configuration file settings, Basic Grayscale Print and Presentation LUT 62 | * are accepted with all uncompressed transfer syntaxes. 63 | * If association negotiation is unsuccessful, an A-ASSOCIATE-RQ is sent 64 | * and the association is dropped. If successful, an A-ASSOCIATE-AC is 65 | * prepared but not (yet) sent. 66 | * @param associatePDU 67 | * @param associatePDUlength 68 | * @return result indicating whether association negotiation was successful. 69 | */ 70 | bool negotiateAssociation(); 71 | 72 | /** confirms an association negotiated with negotiateAssociation() and sends 73 | * all DIMSE communication to upstream printer or process by itself. 74 | * Returns after the association has been released or aborted. 75 | */ 76 | void handleClient(); 77 | 78 | /** destroys the association managed by this object. 79 | */ 80 | void dropAssociations(); 81 | 82 | /** Add attributes from the web service. 83 | * @param rqDataset request dataset, may not be NULL 84 | */ 85 | bool webQuery(DcmDataset *rqDataset); 86 | 87 | private: 88 | 89 | /// private undefined assignment operator 90 | PrintSCP& operator=(const PrintSCP&); 91 | 92 | /// private undefined copy constructor 93 | PrintSCP(const PrintSCP& copy); 94 | 95 | /** sends A-ASSOCIATION-RQ as the result of an unsuccesful association 96 | * negotiation. 97 | * @param isBadContext defines the reason for the A-ASSOCIATE-RQ. 98 | * true indicates an incorrect application context, false sends back 99 | * an unspecified reject with no reason and is used when termination 100 | * of the server application has been initiated. 101 | * @return ASC_NORMAL if successful, an error code otherwise. 102 | */ 103 | OFCondition refuseAssociation(T_ASC_RejectParametersResult result, T_ASC_RejectParametersReason reson); 104 | 105 | /** handles any incoming N-GET-RQ message and sends back N-GET-RSP. 106 | * @param rq request message 107 | * @param rqDataset request dataset, may be NULL 108 | * @param rsp response message 109 | * @param rspDataset response dataset passed back in this parameter (if any) 110 | * @return DIMSE_NORMAL if successful, an error code otherwise 111 | */ 112 | OFCondition handleNGet(T_DIMSE_Message &rq, DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 113 | 114 | /** handles any incoming N-SET-RQ message and sends back N-SET-RSP. 115 | * @param rq request message 116 | * @param rqDataset request dataset, may be NULL 117 | * @param rsp response message 118 | * @param rspDataset response dataset passed back in this parameter (if any) 119 | * @return DIMSE_NORMAL if successful, an error code otherwise 120 | */ 121 | OFCondition handleNSet(T_DIMSE_Message &rq, DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 122 | 123 | /** handles any incoming N-ACTION-RQ message and sends back N-ACTION-RSP. 124 | * @param rq request message 125 | * @param rqDataset request dataset, may be NULL 126 | * @param rsp response message 127 | * @param rspDataset response dataset passed back in this parameter (if any) 128 | * @return DIMSE_NORMAL if successful, an error code otherwise 129 | */ 130 | OFCondition handleNAction(T_DIMSE_Message &rq, DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 131 | 132 | /** handles any incoming N-CREATE-RQ message and sends back N-CREATE-RSP. 133 | * @param rq request message 134 | * @param rqDataset request dataset, may be NULL 135 | * @param rsp response message 136 | * @param rspDataset response dataset passed back in this parameter (if any) 137 | * @return DIMSE_NORMAL if successful, an error code otherwise 138 | */ 139 | OFCondition handleNCreate(T_DIMSE_Message &rq, DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 140 | 141 | /** handles any incoming N-DELETE-RQ message and sends back N-DELETE-RSP. 142 | * @param rq request message 143 | * @param rqDataset request dataset, may be NULL 144 | * @param rsp response message 145 | * @param rspDataset response dataset passed back in this parameter (if any) 146 | * @return DIMSE_NORMAL if successful, an error code otherwise 147 | */ 148 | OFCondition handleNDelete(T_DIMSE_Message &rq, DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 149 | 150 | /** handles any incoming C-ECHO-RQ message and sends back C-ECHO-RSP. 151 | * @param rq request message 152 | * @param rsp response message 153 | * @return DIMSE_NORMAL if successful, an error code otherwise 154 | */ 155 | OFCondition handleCEcho(T_DIMSE_Message &rq, DcmDataset *, T_DIMSE_Message &rsp, DcmDataset *&); 156 | 157 | /** implements the N-GET operation for the Printer SOP Class. 158 | * @param rq request message 159 | * @param rsp response message, already initialized 160 | * @param rspDataset response dataset passed back in this parameter (if any) 161 | */ 162 | void printerNGet(T_DIMSE_Message &rq, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 163 | 164 | /** implements the N-CREATE operation for the Basic Film Session SOP Class. 165 | * @param rqDataset request dataset, may be NULL 166 | * @param rsp response message, already initialized 167 | * @param rspDataset response dataset passed back in this parameter (if any) 168 | */ 169 | void filmSessionNCreate(DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 170 | 171 | /** implements the N-CREATE operation for the Basic Film Box SOP Class. 172 | * @param rqDataset request dataset, may be NULL 173 | * @param rsp response message, already initialized 174 | * @param rspDataset response dataset passed back in this parameter (if any) 175 | */ 176 | void filmBoxNCreate(DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 177 | 178 | /** implements the N-CREATE operation for the Presentation LUT SOP Class. 179 | * @param rqDataset request dataset, may be NULL 180 | * @param rsp response message, already initialized 181 | * @param rspDataset response dataset passed back in this parameter (if any) 182 | */ 183 | void presentationLUTNCreate(DcmDataset *rqDataset, T_DIMSE_Message &rsp, DcmDataset *&rspDataset); 184 | 185 | /** implements the N-DELETE operation for the Basic Film Session SOP Class. 186 | * @param rq request message 187 | * @param rsp response message, already initialized 188 | */ 189 | void filmSessionNDelete(T_DIMSE_Message &rq, T_DIMSE_Message &rsp); 190 | 191 | /** implements the N-DELETE operation for the Basic Film Box SOP Class. 192 | * @param rq request message 193 | * @param rsp response message, already initialized 194 | */ 195 | void filmBoxNDelete(T_DIMSE_Message &rq, T_DIMSE_Message &rsp); 196 | 197 | /** implements the N-DELETE operation for the Presentation LUT SOP Class. 198 | * @param rq request message 199 | * @param rsp response message, already initialized 200 | */ 201 | void presentationLUTNDelete(T_DIMSE_Message &rq, T_DIMSE_Message &rsp); 202 | 203 | /** stores image to the storage servers. 204 | * @param rqDataset request dataset, may not be NULL 205 | */ 206 | void storeImage(DcmDataset *rqDataset); 207 | 208 | /** Add attributes from the printer settings. 209 | * @param rqDataset request dataset, may not be NULL 210 | * @param queryParams for the web service 211 | * @param di image from dataset, may not be NULL 212 | * @param settings to read attributes from 213 | */ 214 | void insertTags(DcmDataset *rqDataset, QVariantMap &queryParams, DicomImage *di, QSettings &settings); 215 | 216 | void dump(const char* desc, DcmItem *dataset); 217 | void dumpIn(T_DIMSE_Message &msg, DcmItem *dataset); 218 | void dumpOut(T_DIMSE_Message &msg, DcmItem *dataset); 219 | 220 | /* class data */ 221 | 222 | // blocking mode for receive 223 | // 224 | T_DIMSE_BlockingMode blockMode; 225 | 226 | // timeout for receive 227 | // 228 | int timeout; 229 | 230 | // basic film session instance 231 | // 232 | QString studyInstanceUID; 233 | QString seriesInstanceUID; 234 | QString SOPInstanceUID; 235 | 236 | // Workarounds for some spooler "optimizatins" 237 | // 238 | bool forceUniqueSeries; 239 | bool forceUniqueStudy; 240 | 241 | // the dataset to log all session attributes 242 | // 243 | DcmDataset* sessionDataset; 244 | 245 | // Printer AETITLE. Must have a section in the settings file 246 | // 247 | QString printer; 248 | 249 | // the DICOM network and listen port 250 | // 251 | T_ASC_Network *upstreamNet; 252 | 253 | // the network association over which the print SCP is operating 254 | // 255 | T_ASC_Association *assoc; 256 | 257 | // the network association over which the real printer is operating 258 | // 259 | T_ASC_Association *upstream; 260 | 261 | #ifdef WITH_TESSERACT 262 | // OCR 263 | // 264 | tesseract::TessBaseAPI tess; 265 | #endif 266 | 267 | // Log upstream printer traffic (off by default) 268 | // 269 | bool debugUpstream; 270 | 271 | // Regular expression to remove non printable symbols 272 | // For example, [^a-zA-Z .] will remove everything 273 | // except latin chars, the dot and the space. 274 | // "W PE=RAE=RAE=s-HK<©" will be "W PERAERAEsHK" 275 | // 276 | QRegExp reBadSymbols; 277 | }; 278 | 279 | bool saveToDisk(const QString& spoolPath, DcmDataset* rqDataset); 280 | 281 | #endif // PRINTSCP_H 282 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #include "product.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #ifdef UNICODE 25 | #define DCMTK_UNICODE_BUG_WORKAROUND 26 | #undef UNICODE 27 | #endif 28 | 29 | #include "printscp.h" 30 | #include "storescp.h" 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | // DCMTK prior to 3.6.1 has no its own namespace. 38 | namespace dcmtk{} 39 | using namespace dcmtk; 40 | 41 | #ifdef DCMTK_UNICODE_BUG_WORKAROUND 42 | #define UNICODE 43 | #undef DCMTK_UNICODE_BUG_WORKAROUND 44 | #endif 45 | 46 | #define DEFAULT_SPOOL_INTERVAL 600 47 | 48 | static int resendWorkerPid = 0; 49 | 50 | static void cleanChildren() 51 | { 52 | qDebug() << __func__; 53 | 54 | #ifdef HAVE_WAITPID 55 | int status; 56 | #elif HAVE_WAIT3 57 | struct rusage rusage; 58 | #if defined(__NeXT__) 59 | // some systems need a union wait as argument to wait3 60 | union wait status; 61 | #else 62 | int status; 63 | #endif 64 | #endif 65 | 66 | #if defined(HAVE_WAITPID) || defined(HAVE_WAIT3) 67 | int child = 1; 68 | int options = WNOHANG; 69 | while (child > 0) 70 | { 71 | #ifdef HAVE_WAITPID 72 | child = (int)(waitpid(-1, &status, options)); 73 | #elif defined(HAVE_WAIT3) 74 | child = wait3(&status, options, &rusage); 75 | #endif 76 | if (child < 0) 77 | { 78 | if (errno != ECHILD) 79 | { 80 | qDebug() << "wait for child failed: " << QString::fromLocal8Bit(strerror(errno)); 81 | } 82 | } 83 | else if (child > 0) 84 | { 85 | qDebug() << "Child process" << child << "terminated with status" << status; 86 | if (resendWorkerPid == child) 87 | { 88 | resendWorkerPid = 0; 89 | } 90 | } 91 | 92 | } 93 | #endif 94 | } 95 | 96 | static bool resendFailedPrints(QSettings& settings) 97 | { 98 | // Retry failed prints 99 | // 100 | auto spoolPath = settings.value("spool-path").toString(); 101 | if (spoolPath.isEmpty()) 102 | { 103 | // Retry spool is disabled 104 | return false; 105 | } 106 | 107 | if (QDateTime::currentDateTime() < settings.value("next-spool-ts").toDateTime()) 108 | { 109 | // Not yet. May be next time 110 | qDebug() << __func__ << "delayed"; 111 | return false; 112 | } 113 | 114 | auto spoolInterval = settings.value("spool-interval-in-seconds", DEFAULT_SPOOL_INTERVAL).toInt(); 115 | settings.setValue("next-spool-ts", QDateTime::currentDateTime().addSecs(spoolInterval)); 116 | 117 | // Save it immediatelly to avoid corruption. 118 | // 119 | settings.sync(); 120 | 121 | #ifdef HAVE_FORK 122 | if (resendWorkerPid > 0) 123 | { 124 | qDebug() << "Worker process" << resendWorkerPid << "is still alive, resend delayed"; 125 | return false; 126 | } 127 | 128 | resendWorkerPid = fork(); 129 | 130 | if (resendWorkerPid > 0) 131 | { 132 | qDebug() << "Worker process to resend failed prints spawned. Pid" << resendWorkerPid; 133 | return false; 134 | } 135 | #endif 136 | 137 | // Really start to process failed prints 138 | // 139 | OFCondition cond; 140 | 141 | // Retry failed web queries 142 | // 143 | qDebug() << __func__ << "retrying prints"; 144 | Q_FOREACH (auto file, QDir(spoolPath).entryInfoList(QDir::Files)) 145 | { 146 | auto filePath = file.absoluteFilePath(); 147 | qDebug() << "Retrying " << filePath; 148 | 149 | DcmFileFormat dcmFF; 150 | cond = dcmFF.loadFile((const char*)filePath.toLocal8Bit()); 151 | if (cond.bad()) 152 | { 153 | qDebug() << "Failed to load " << filePath << ": " << QString::fromLocal8Bit(cond.text()); 154 | continue; 155 | } 156 | 157 | const char* printer = nullptr; 158 | dcmFF.getDataset()->findAndGetString(DCM_RETIRED_PrintQueueID, printer); 159 | if (printer == nullptr) 160 | { 161 | qDebug() << "Failed to retry " << filePath << ": no printer instance specified"; 162 | continue; 163 | } 164 | 165 | PrintSCP retryPrintSCP(nullptr, nullptr, QString::fromUtf8(printer)); 166 | 167 | if (retryPrintSCP.webQuery(dcmFF.getDataset())) 168 | { 169 | foreach (auto server, settings.value("storage-servers").toStringList()) 170 | { 171 | StoreSCP sscp(server); 172 | const char* SOPInstanceUID = nullptr; 173 | dcmFF.getDataset()->findAndGetString(DCM_SOPInstanceUID, SOPInstanceUID); 174 | 175 | cond = sscp.sendToServer(dcmFF.getDataset(), SOPInstanceUID); 176 | if (cond.bad()) 177 | { 178 | // The Web query secceded, but store failed. 179 | // Move the file down to the queue. 180 | // At this point, we will copy the dataset as many times, 181 | // as need for each failed store server. 182 | // 183 | saveToDisk(QString(spoolPath).append(QDir::separator()).append(server), dcmFF.getDataset()); 184 | } 185 | } 186 | 187 | if (!QFile::remove(filePath)) 188 | { 189 | qDebug() << "Failed to remove file " << filePath 190 | << ": " << QString::fromLocal8Bit(strerror(errno)); 191 | } 192 | } 193 | } 194 | 195 | qDebug() << __func__ << "retrying dcmstore"; 196 | foreach (auto server, settings.value("storage-servers").toStringList()) 197 | { 198 | StoreSCP sscp(server); 199 | auto path = QDir(QString(spoolPath).append(QDir::separator()).append(server)); 200 | Q_FOREACH (auto file, path.entryInfoList(QDir::Files)) 201 | { 202 | auto filePath = file.absoluteFilePath(); 203 | qDebug() << "Resending " << filePath; 204 | 205 | DcmFileFormat dcmFF; 206 | cond = dcmFF.loadFile((const char*)filePath.toLocal8Bit()); 207 | if (cond.bad()) 208 | { 209 | qDebug() << "Failed to load " << filePath 210 | << ": " << QString::fromLocal8Bit(cond.text()); 211 | continue; 212 | } 213 | 214 | const char* SOPInstanceUID = nullptr; 215 | dcmFF.getDataset()->findAndGetString(DCM_SOPInstanceUID, SOPInstanceUID); 216 | 217 | cond = sscp.sendToServer(dcmFF.getDataset(), SOPInstanceUID); 218 | if (cond.good()) 219 | { 220 | if (!QFile::remove(filePath)) 221 | { 222 | qDebug() << "Failed to remove file " << filePath 223 | << ": " << QString::fromLocal8Bit(strerror(errno)); 224 | } 225 | } 226 | } 227 | } 228 | qDebug() << __func__ << "done"; 229 | return true; 230 | } 231 | 232 | void handleClient(T_ASC_Association *assoc) 233 | { 234 | PrintSCP printSCP(assoc); 235 | 236 | if (printSCP.negotiateAssociation()) 237 | { 238 | printSCP.handleClient(); 239 | } 240 | } 241 | 242 | int main(int argc, char *argv[]) 243 | { 244 | QCoreApplication app(argc, argv); 245 | app.setApplicationName(PRODUCT_SHORT_NAME); 246 | app.setOrganizationName(ORGANIZATION_DOMAIN); 247 | 248 | QUtf8Settings settings; 249 | auto logLevel = settings.value("log-level").toString(); 250 | if (!logLevel.isEmpty()) 251 | { 252 | auto level = log4cplus::getLogLevelManager().fromString(logLevel.toUtf8().constData()); 253 | log4cplus::Logger::getRoot().setLogLevel(level); 254 | } 255 | 256 | auto debugUpstream = settings.value("debug-upstream").toBool(); 257 | if (debugUpstream) 258 | { 259 | log4cplus::Logger log = log4cplus::Logger::getInstance("dcmtk.dcmpstat.dump"); 260 | log.setLogLevel(OFLogger::DEBUG_LOG_LEVEL); 261 | } 262 | 263 | auto port = settings.value("port", DEFAULT_LISTEN_PORT).toInt(); 264 | auto tout = settings.value("timeout", DEFAULT_TIMEOUT).toInt(); 265 | T_ASC_Network *net; 266 | OFCondition cond = ASC_initializeNetwork(NET_ACCEPTOR, port, tout, &net); 267 | 268 | // When the listener process has been recently crashed, 269 | // the port will be busy for some time (see CLOSE_WAIT). 270 | // 271 | for (int i = 0; cond.bad() && i < 20; ++i) 272 | { 273 | usleep(200000); 274 | cond = ASC_initializeNetwork(NET_ACCEPTOR, port, tout, &net); 275 | } 276 | 277 | if (cond.bad()) 278 | { 279 | qWarning() << "cannot initialize network" << QString::fromLocal8Bit(cond.text()); 280 | return 1; 281 | } 282 | 283 | #if defined(HAVE_SETUID) && defined(HAVE_GETUID) 284 | // Return to normal uid so that we can't do too much damage in case 285 | // things go very wrong. Only relevant if the program is setuid root, 286 | // and run by another user. Running as root user may be 287 | // potentially disasterous if this program screws up badly. 288 | // 289 | auto err = setuid(getuid()); 290 | if (err) 291 | { 292 | qWarning() << "setuid failed" << errno << "this may be dangerous"; 293 | } 294 | #endif 295 | 296 | #ifdef HAVE_FORK 297 | int listen_timeout=10; 298 | #else 299 | int listen_timeout=10000; 300 | #endif 301 | 302 | qCritical() << "Virtual DICOM printer version" << PRODUCT_VERSION_STR 303 | << "started. Master process pid" << getpid(); 304 | 305 | Q_FOREVER 306 | { 307 | do 308 | { 309 | cleanChildren(); 310 | if (resendFailedPrints(settings)) 311 | { 312 | // Resend worker routine has been completed 313 | return 0; 314 | } 315 | qDebug() << "waiting for connection"; 316 | } 317 | while (!ASC_associationWaiting(net, listen_timeout)); 318 | 319 | qDebug() << "Client connected"; 320 | 321 | T_ASC_Association *assoc = nullptr; 322 | cond = ASC_receiveAssociation(net, &assoc, DEFAULT_MAXPDU); 323 | if (cond.bad()) 324 | { 325 | qWarning() << "Failed to receive association"; 326 | ASC_dropSCPAssociation(assoc); 327 | ASC_destroyAssociation(&assoc); 328 | continue; 329 | } 330 | 331 | #if defined(HAVE_FORK) && !defined(QT_DEBUG) 332 | auto pid = fork(); 333 | if (pid < 0) 334 | { 335 | qDebug() << "fork() failed, err" << errno 336 | << "\nWill handle client connection in the main process"; 337 | handleClient(assoc); 338 | } 339 | else if (pid == 0) 340 | { 341 | // Do the real work. 342 | // 343 | handleClient(assoc); 344 | qDebug() << "Child process completed. pid" << getpid(); 345 | return 0; 346 | } 347 | else 348 | { 349 | qDebug() << "Child process" << pid << "spawned"; 350 | } 351 | #else 352 | qDebug() << "Will handle client connection in the main process"; 353 | handleClient(assoc); 354 | #endif 355 | } 356 | 357 | return 0; 358 | } 359 | -------------------------------------------------------------------------------- /transcyrillic.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #if defined(_MSC_VER) 18 | #pragma execution_character_set("utf-8") 19 | #endif 20 | #include "transcyrillic.h" 21 | 22 | static inline int next(const QString& str, int idx) 23 | { 24 | return str.length() > ++idx? str[idx].toLower().unicode(): 0; 25 | } 26 | 27 | QString translateToCyrillic(const QString& str) 28 | { 29 | if (str.isNull()) 30 | { 31 | return str; 32 | } 33 | 34 | QString ret; 35 | 36 | for (int i = 0; i < str.length(); ++i) 37 | { 38 | switch (str[i].unicode()) 39 | { 40 | case '^': 41 | ret.append(' '); // Заменяем шапку на пробел 42 | break; 43 | case 'A': 44 | ret.append(L'А'); 45 | break; 46 | case 'B': 47 | ret.append(L'Б'); 48 | break; 49 | case 'V': 50 | ret.append(L'В'); 51 | break; 52 | case 'G': 53 | ret.append(L'Г'); 54 | break; 55 | case 'D': 56 | ret.append(L'Д'); 57 | break; 58 | case 'E': 59 | ret.append(i==0?L'Э':L'Е'); // В начале слова русская 'е' это 'ye' 60 | break; 61 | case 'Z': 62 | ret.append(next(str, i) == 'h'? ++i, L'Ж': L'З'); 63 | break; 64 | case 'I': 65 | ret.append(L'И'); 66 | break; 67 | case 'K': 68 | ret.append(next(str, i) == 'h'? ++i, L'Х': L'К'); 69 | break; 70 | case 'L': 71 | ret.append(L'Л'); 72 | break; 73 | case 'M': 74 | ret.append(L'М'); 75 | break; 76 | case 'N': 77 | ret.append(L'Н'); 78 | break; 79 | case 'O': 80 | ret.append(L'О'); 81 | break; 82 | case 'P': 83 | ret.append(L'П'); 84 | break; 85 | case 'R': 86 | ret.append(L'Р'); 87 | break; 88 | case 'S': 89 | if (next(str, i) == 'h') 90 | { 91 | ++i; 92 | if (next(str, i) == 'c' && next(str, i + 1) == 'h') 93 | { 94 | i+=2; 95 | ret.append(L'Щ'); 96 | } 97 | else 98 | { 99 | ret.append(L'Ш'); 100 | } 101 | } 102 | else 103 | { 104 | ret.append(L'С'); 105 | } 106 | break; 107 | case 'T': 108 | ret.append(next(str, i) == 's'? ++i, L'Ц': L'Т'); 109 | break; 110 | case 'U': 111 | ret.append(L'У'); 112 | break; 113 | case 'F': 114 | ret.append(L'Ф'); 115 | break; 116 | case 'X': 117 | ret.append(L'К').append(i < str.length() - 1 && str[i+1].isLower()? L'с': L'С'); 118 | break; 119 | case 'C': 120 | ret.append(next(str, i) == 'h'? ++i, L'Ч': L'К'); // Английская 'c' без 'h' не должна встречаться. 121 | break; 122 | case 'Y': 123 | switch (next(str, i)) { 124 | case 'e': 125 | ++i, ret.append(L'Е'); 126 | break; 127 | case 'o': 128 | ++i, ret.append(L'Ё'); 129 | break; 130 | case 'u': 131 | ++i, ret.append(L'Ю'); 132 | break; 133 | case 'i': 134 | ret.append(L'Ь'); // Только для мягкого знака перед 'и', как Ilyin. 135 | break; 136 | case 'a': 137 | ++i, ret.append(L'Я'); 138 | break; 139 | case '\x0': 140 | case ' ': 141 | case '.': 142 | ret.append(L'И').append(L'Й'); // Фамилии на ый заканчиваются редко 143 | break; 144 | default: 145 | ret.append(L'Ы'); 146 | break; 147 | } 148 | break; 149 | 150 | case 'a': 151 | ret.append(L'а'); 152 | break; 153 | case 'b': 154 | ret.append(L'б'); 155 | break; 156 | case 'v': 157 | ret.append(L'в'); 158 | break; 159 | case 'g': 160 | ret.append(L'г'); 161 | break; 162 | case 'd': 163 | ret.append(L'д'); 164 | break; 165 | case 'e': 166 | ret.append(i==0?L'э':L'е'); // В начале слова русская 'е' это 'ye' 167 | break; 168 | case 'z': 169 | ret.append(next(str, i) == 'h'? ++i, L'ж': L'з'); 170 | break; 171 | case 'i': 172 | ret.append(L'и'); 173 | break; 174 | case 'k': 175 | ret.append(next(str, i) == 'h'? ++i, L'х': L'к'); 176 | break; 177 | case 'l': 178 | ret.append(L'л'); 179 | break; 180 | case 'm': 181 | ret.append(L'м'); 182 | break; 183 | case 'n': 184 | ret.append(L'н'); 185 | break; 186 | case 'o': 187 | ret.append(L'о'); 188 | break; 189 | case 'p': 190 | ret.append(L'п'); 191 | break; 192 | case 'r': 193 | ret.append(L'р'); 194 | break; 195 | case 's': 196 | if (next(str, i) == 'h') 197 | { 198 | ++i; 199 | if (next(str, i) == 'c' && next(str, i + 1) == 'h') 200 | { 201 | i+=2; 202 | ret.append(L'щ'); 203 | } 204 | else 205 | { 206 | ret.append(L'ш'); 207 | } 208 | } 209 | else 210 | { 211 | ret.append(L'с'); 212 | } 213 | break; 214 | case 't': 215 | ret.append(next(str, i) == 's'? ++i, L'ц': L'т'); 216 | break; 217 | case 'u': 218 | ret.append(L'у'); 219 | break; 220 | case 'f': 221 | ret.append(L'ф'); 222 | break; 223 | case 'x': 224 | ret.append(L'к').append(L'с'); 225 | break; 226 | case 'c': 227 | ret.append(next(str, i) == 'h'? ++i, L'ч': L'к'); // Английская 'c' без 'h' не должна встречаться. 228 | break; 229 | case 'y': 230 | switch (next(str, i)) { 231 | case 'e': 232 | ++i, ret.append(L'е'); 233 | break; 234 | case 'o': 235 | ++i, ret.append(L'ё'); 236 | break; 237 | case 'u': 238 | ++i, ret.append(L'ю'); 239 | break; 240 | case 'i': 241 | ret.append(L'ь'); // Только для мягкого знака перед 'и', как Ilyin. 242 | break; 243 | case 'a': 244 | ++i, ret.append(L'я'); 245 | break; 246 | case '\x0': 247 | case ' ': 248 | case '.': 249 | ret.append(L'и').append(L'й'); // Фамилии на ый заканчиваются редко 250 | break; 251 | default: 252 | ret.append(L'ы'); 253 | break; 254 | } 255 | break; 256 | 257 | default: 258 | ret.append(str[i]); // Прочие символы без изменений 259 | break; 260 | } 261 | } 262 | 263 | return ret; 264 | } 265 | 266 | QString translateToLatin(const QString& str) 267 | { 268 | if (str.isNull()) 269 | { 270 | return str; 271 | } 272 | 273 | QString ret; 274 | 275 | for (int i = 0; i < str.length(); ++i) 276 | { 277 | switch (str[i].unicode()) 278 | { 279 | // case ' ': 280 | // ret.append('^'); // Заменяем пробел на шапку 281 | // break; 282 | case L'А': 283 | ret.append('A'); 284 | break; 285 | case L'Б': 286 | ret.append('B'); 287 | break; 288 | case L'В': 289 | ret.append('V'); 290 | break; 291 | case L'Г': 292 | ret.append('G'); 293 | break; 294 | case L'Д': 295 | ret.append('D'); 296 | break; 297 | case L'Е': 298 | if (i==0) // В начале слова русская 'е' это 'ye' или 'YE' 299 | { 300 | ret.append('Y'); 301 | ret.append(str.length() > 1 && str[1].isLower()? 'e': 'E'); 302 | } 303 | else 304 | { 305 | ret.append('E'); 306 | } 307 | break; 308 | case L'Ё': 309 | ret.append('Y'); 310 | ret.append(str.length() > i && str[i].isLower()? 'o': 'O'); 311 | break; 312 | case L'Ж': 313 | ret.append('Z'); 314 | ret.append(str.length() > i && str[i].isLower()? 'h': 'H'); 315 | break; 316 | case L'З': 317 | ret.append('Z'); 318 | break; 319 | case L'И': // В конце слова 'ий' == 'y' 320 | if (str.length() > i && str[i] == L'Й') 321 | { 322 | ret.append('Y'); ++i; 323 | } 324 | else if (str.length() > i && str[i] == L'й') 325 | { 326 | ret.append('y'); ++i; 327 | } 328 | else 329 | { 330 | ret.append('I'); 331 | } 332 | break; 333 | case L'Й': 334 | ret.append('Y'); 335 | break; 336 | case L'К': 337 | ret.append('K'); 338 | break; 339 | case L'Л': 340 | ret.append('L'); 341 | break; 342 | case L'М': 343 | ret.append('M'); 344 | break; 345 | case L'Н': 346 | ret.append('N'); 347 | break; 348 | case L'О': 349 | ret.append('O'); 350 | break; 351 | case L'П': 352 | ret.append('P'); 353 | break; 354 | case L'Р': 355 | ret.append('R'); 356 | break; 357 | case L'С': 358 | ret.append('S'); 359 | break; 360 | case L'Т': 361 | ret.append('T'); 362 | break; 363 | case L'У': 364 | ret.append('U'); 365 | break; 366 | case L'Ф': 367 | ret.append('F'); 368 | break; 369 | case L'Х': 370 | ret.append('K'); 371 | ret.append(str.length() > i && str[i].isLower()? 'h': 'H'); 372 | break; 373 | case L'Ц': 374 | ret.append('T'); 375 | ret.append(str.length() > i && str[i].isLower()? 's': 'S'); 376 | break; 377 | case L'Ч': 378 | ret.append('C'); 379 | ret.append(str.length() > i && str[i].isLower()? 'h': 'H'); 380 | break; 381 | case L'Ш': 382 | ret.append('S'); 383 | ret.append(str.length() > i && str[i].isLower()? 'h': 'H'); 384 | break; 385 | case L'Щ': 386 | ret.append('S'); 387 | ret.append(str.length() > i && str[i].isLower()? "hch": "HCH"); 388 | break; 389 | case L'Ъ': // Съедаем 390 | break; 391 | case L'Ы': // В конце слова 'ый' == 'y' 392 | ret.append('Y'); 393 | if (str.length() > i && str[i] == L'Й') 394 | { 395 | ++i; 396 | } 397 | break; 398 | case L'Ь': // Только для мягкого знака перед 'и', как Ilyin. 399 | if (str.length() > i && str[i] == L'И') 400 | { 401 | ret.append('Y'); 402 | } 403 | else if (str.length() > i && str[i] == L'и') 404 | { 405 | ret.append('y'); 406 | } 407 | break; 408 | case L'Э': 409 | ret.append('E'); 410 | break; 411 | case L'Ю': 412 | ret.append('Y'); 413 | ret.append(str.length() > i && str[i].isLower()? 'u': 'U'); 414 | break; 415 | case L'Я': 416 | ret.append('Y'); 417 | ret.append(str.length() > i && str[i].isLower()? 'a': 'A'); 418 | break; 419 | 420 | case L'а': 421 | ret.append('a'); 422 | break; 423 | case L'б': 424 | ret.append('b'); 425 | break; 426 | case L'в': 427 | ret.append('v'); 428 | break; 429 | case L'г': 430 | ret.append('g'); 431 | break; 432 | case L'д': 433 | ret.append('d'); 434 | break; 435 | case L'е': 436 | if (i==0) // В начале слова русская 'е' это 'ye' или 'YE' 437 | { 438 | ret.append('y'); 439 | } 440 | ret.append('e'); 441 | break; 442 | case L'ё': 443 | ret.append("yo"); 444 | break; 445 | case L'ж': 446 | ret.append("zh"); 447 | break; 448 | case L'з': 449 | ret.append('z'); 450 | break; 451 | case L'и': // В конце слова 'ий' == 'y' 452 | if (str.length() > i && str[i] == L'й') 453 | { 454 | ret.append('y'); ++i; 455 | } 456 | else 457 | { 458 | ret.append('i'); 459 | } 460 | break; 461 | case L'й': 462 | ret.append('y'); 463 | break; 464 | case L'к': 465 | ret.append('k'); 466 | break; 467 | case L'л': 468 | ret.append('l'); 469 | break; 470 | case L'м': 471 | ret.append('m'); 472 | break; 473 | case L'н': 474 | ret.append('n'); 475 | break; 476 | case L'о': 477 | ret.append('o'); 478 | break; 479 | case L'п': 480 | ret.append('p'); 481 | break; 482 | case L'р': 483 | ret.append('r'); 484 | break; 485 | case L'с': 486 | ret.append('s'); 487 | break; 488 | case L'т': 489 | ret.append('t'); 490 | break; 491 | case L'у': 492 | ret.append('u'); 493 | break; 494 | case L'ф': 495 | ret.append('f'); 496 | break; 497 | case L'х': 498 | ret.append("kh"); 499 | break; 500 | case L'ц': 501 | ret.append("tc"); 502 | break; 503 | case L'ч': 504 | ret.append("ch"); 505 | break; 506 | case L'ш': 507 | ret.append("sh"); 508 | break; 509 | case L'щ': 510 | ret.append("shch"); 511 | break; 512 | case L'ъ': // Съедаем 513 | break; 514 | case L'ы': // В конце слова 'ый' == 'y' 515 | ret.append('y'); 516 | if (str.length() > i && str[i] == L'й') 517 | { 518 | ++i; 519 | } 520 | break; 521 | case L'ь': // Только для мягкого знака перед 'и', как Ilyin. 522 | if (str.length() > i && str[i] == L'и') 523 | { 524 | ret.append('y'); 525 | } 526 | break; 527 | case L'э': 528 | ret.append('e'); 529 | break; 530 | case L'ю': 531 | ret.append("yu"); 532 | break; 533 | case L'я': 534 | ret.append("ya"); 535 | break; 536 | 537 | default: 538 | ret.append(str[i]); // Прочие символы без изменений 539 | break; 540 | } 541 | } 542 | 543 | return ret; 544 | } 545 | -------------------------------------------------------------------------------- /printscp.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014-2018 Softus Inc. 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation; version 2. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU Lesser General Public License 14 | * along with this program. If not, see . 15 | */ 16 | 17 | #include "product.h" 18 | #include "printscp.h" 19 | #include "storescp.h" 20 | #include "transcyrillic.h" 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 29 | #include 30 | #include 31 | #include 32 | #endif 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #include // Required for tesseract 39 | 40 | #ifdef UNICODE 41 | #define DCMTK_UNICODE_BUG_WORKAROUND 42 | #undef UNICODE 43 | #endif 44 | 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include /* for constants */ 50 | #include /* for DicomImage */ 51 | 52 | #ifdef DCMTK_UNICODE_BUG_WORKAROUND 53 | #define UNICODE 54 | #undef DCMTK_UNICODE_BUG_WORKAROUND 55 | #endif 56 | 57 | #ifndef DCM_RETIRED_DestinationAE 58 | #define DCM_RETIRED_DestinationAE DcmTagKey(0x2100, 0x0140) 59 | #endif 60 | 61 | bool saveToDisk(const QString& spoolPath, DcmDataset* rqDataset) 62 | { 63 | if (!QDir::root().mkpath(spoolPath)) 64 | { 65 | qDebug() << "Failed to create folder " << spoolPath << ": " << QString::fromLocal8Bit(strerror(errno)); 66 | } 67 | 68 | const char* uId = nullptr; 69 | rqDataset->findAndGetString(DCM_SOPInstanceUID, uId); 70 | QString fileName = QString(spoolPath).append(QDir::separator()).append(uId).append(".dcm"); 71 | 72 | if (QFile::exists(fileName)) 73 | { 74 | int cnt = 1; 75 | QString alt; 76 | do 77 | { 78 | alt = QString(fileName).append(" (").append(QString::number(++cnt)).append(')'); 79 | } 80 | while (QFile::exists(alt)); 81 | fileName = alt; 82 | } 83 | 84 | DcmFileFormat ff(rqDataset); 85 | OFCondition cond = ff.saveFile((const char*)fileName.toUtf8(), 86 | EXS_LittleEndianExplicit, EET_ExplicitLength, EGL_recalcGL, EPD_withoutPadding); 87 | 88 | if (cond.bad()) 89 | { 90 | qDebug() << "Failed to save " << fileName << ": " << QString::fromLocal8Bit(cond.text()); 91 | } 92 | else 93 | { 94 | qDebug() << "Dataset saved to " << fileName; 95 | } 96 | 97 | return cond.good(); 98 | } 99 | 100 | static OFCondition putAndInsertVariant(DcmDataset* dataset, const DcmTag& tag, const QVariant& value) 101 | { 102 | switch (tag.getEVR()) 103 | { 104 | case EVR_FL: 105 | case EVR_OF: 106 | return dataset->putAndInsertFloat32(tag, value.toFloat()); 107 | case EVR_FD: 108 | return dataset->putAndInsertFloat64(tag, value.toDouble()); 109 | case EVR_SL: 110 | return dataset->putAndInsertSint32(tag, value.toInt()); 111 | case EVR_UL: 112 | return dataset->putAndInsertUint32(tag, value.toUInt()); 113 | case EVR_SS: 114 | return dataset->putAndInsertSint16(tag, (Sint16)value.toInt()); 115 | case EVR_US: 116 | return dataset->putAndInsertUint16(tag, (Uint16)value.toUInt()); 117 | case EVR_DA: 118 | return dataset->putAndInsertString(tag, value.toDate().toString("yyyyMMdd").toUtf8()); 119 | case EVR_DT: 120 | return dataset->putAndInsertString(tag, value.toDateTime().toString("yyyyMMddHHmmss").toUtf8()); 121 | case EVR_TM: 122 | return dataset->putAndInsertString(tag, value.toTime().toString("HHmmss").toUtf8()); 123 | default: 124 | if (tag.getVR().isaString()) 125 | { 126 | return dataset->putAndInsertString(tag, value.toString().toUtf8()); 127 | } 128 | break; 129 | } 130 | 131 | qDebug() << "VR" << tag.getVRName() << "not implemented"; 132 | return EC_IllegalParameter; 133 | } 134 | 135 | static OFCondition findAndGetVariant(DcmDataset* dataset, const DcmTag& tag, QVariant& value) 136 | { 137 | OFCondition cond; 138 | switch (tag.getEVR()) 139 | { 140 | case EVR_FL: 141 | case EVR_OF: 142 | { 143 | float f = 0.0f; 144 | cond = dataset->findAndGetFloat32(tag, f); 145 | if (cond.good()) { value.setValue(f); } 146 | break; 147 | } 148 | case EVR_FD: 149 | { 150 | double d = 0.0; 151 | cond = dataset->findAndGetFloat64(tag, d); 152 | if (cond.good()) { value.setValue(d); } 153 | break; 154 | } 155 | case EVR_SL: 156 | { 157 | Sint32 i = 0; 158 | cond = dataset->findAndGetSint32(tag, i); 159 | if (cond.good()) { value.setValue(i); } 160 | break; 161 | } 162 | case EVR_UL: 163 | { 164 | Uint32 u = 0; 165 | cond = dataset->findAndGetUint32(tag, u); 166 | if (cond.good()) { value.setValue(u); } 167 | break; 168 | } 169 | case EVR_SS: 170 | { 171 | Sint16 i = 0; 172 | cond = dataset->findAndGetSint16(tag, i); 173 | if (cond.good()) { value.setValue(i); } 174 | break; 175 | } 176 | case EVR_US: 177 | { 178 | Uint16 u = 0; 179 | cond = dataset->findAndGetUint16(tag, u); 180 | if (cond.good()) { value.setValue(u); } 181 | break; 182 | } 183 | case EVR_DA: 184 | { 185 | const char* str = nullptr; 186 | cond = dataset->findAndGetString(tag, str); 187 | if (cond.good()) 188 | { 189 | value.setValue(QDate::fromString(str, "yyyyMMdd")); 190 | } 191 | break; 192 | } 193 | case EVR_DT: 194 | { 195 | const char* str = nullptr; 196 | cond = dataset->findAndGetString(tag, str); 197 | if (cond.good()) 198 | { 199 | value.setValue(QDateTime::fromString(str, "yyyyMMddHHmmss")); 200 | } 201 | break; 202 | } 203 | case EVR_TM: 204 | { 205 | const char* str = nullptr; 206 | cond = dataset->findAndGetString(tag, str); 207 | if (cond.good()) 208 | { 209 | value.setValue(QTime::fromString(str, "HHmmss")); 210 | } 211 | break; 212 | } 213 | default: 214 | if (tag.getVR().isaString()) 215 | { 216 | const char* str = nullptr; 217 | cond = dataset->findAndGetString(tag, str); 218 | if (cond.good()) 219 | { 220 | value.setValue(QString::fromUtf8(str)); 221 | } 222 | } 223 | else 224 | { 225 | qDebug() << "VR" << tag.getVRName() << "not implemented"; 226 | cond = EC_IllegalParameter; 227 | break; 228 | } 229 | } 230 | 231 | return cond; 232 | } 233 | 234 | static bool isDatasetPresent(T_DIMSE_Message &msg) 235 | { 236 | switch (msg.CommandField) 237 | { 238 | case DIMSE_C_STORE_RQ: return msg.msg.CStoreRQ.DataSetType != DIMSE_DATASET_NULL; 239 | case DIMSE_C_STORE_RSP: return msg.msg.CStoreRSP.DataSetType != DIMSE_DATASET_NULL; 240 | case DIMSE_C_GET_RQ: return msg.msg.CGetRQ.DataSetType != DIMSE_DATASET_NULL; 241 | case DIMSE_C_GET_RSP: return msg.msg.CGetRSP.DataSetType != DIMSE_DATASET_NULL; 242 | case DIMSE_C_FIND_RQ: return msg.msg.CFindRQ.DataSetType != DIMSE_DATASET_NULL; 243 | case DIMSE_C_FIND_RSP: return msg.msg.CFindRSP.DataSetType != DIMSE_DATASET_NULL; 244 | case DIMSE_C_MOVE_RQ: return msg.msg.CMoveRQ.DataSetType != DIMSE_DATASET_NULL; 245 | case DIMSE_C_MOVE_RSP: return msg.msg.CMoveRSP.DataSetType != DIMSE_DATASET_NULL; 246 | case DIMSE_C_ECHO_RQ: return msg.msg.CEchoRQ.DataSetType != DIMSE_DATASET_NULL; 247 | case DIMSE_C_ECHO_RSP: return msg.msg.CEchoRSP.DataSetType != DIMSE_DATASET_NULL; 248 | case DIMSE_C_CANCEL_RQ: return msg.msg.CCancelRQ.DataSetType != DIMSE_DATASET_NULL; 249 | /* there is no DIMSE_C_CANCEL_RSP */ 250 | 251 | case DIMSE_N_EVENT_REPORT_RQ: return msg.msg.NEventReportRQ.DataSetType != DIMSE_DATASET_NULL; 252 | case DIMSE_N_EVENT_REPORT_RSP: return msg.msg.NEventReportRSP.DataSetType != DIMSE_DATASET_NULL; 253 | case DIMSE_N_GET_RQ: return msg.msg.NGetRQ.DataSetType != DIMSE_DATASET_NULL; 254 | case DIMSE_N_GET_RSP: return msg.msg.NGetRSP.DataSetType != DIMSE_DATASET_NULL; 255 | case DIMSE_N_SET_RQ: return msg.msg.NSetRQ.DataSetType != DIMSE_DATASET_NULL; 256 | case DIMSE_N_SET_RSP: return msg.msg.NSetRSP.DataSetType != DIMSE_DATASET_NULL; 257 | case DIMSE_N_ACTION_RQ: return msg.msg.NActionRQ.DataSetType != DIMSE_DATASET_NULL; 258 | case DIMSE_N_ACTION_RSP: return msg.msg.NActionRSP.DataSetType != DIMSE_DATASET_NULL; 259 | case DIMSE_N_CREATE_RQ: return msg.msg.NCreateRQ.DataSetType != DIMSE_DATASET_NULL; 260 | case DIMSE_N_CREATE_RSP: return msg.msg.NCreateRSP.DataSetType != DIMSE_DATASET_NULL; 261 | case DIMSE_N_DELETE_RQ: return msg.msg.NDeleteRQ.DataSetType != DIMSE_DATASET_NULL; 262 | case DIMSE_N_DELETE_RSP: return msg.msg.NDeleteRSP.DataSetType != DIMSE_DATASET_NULL; 263 | 264 | default: 265 | qDebug() << "Unhandled command field" << msg.CommandField; 266 | break; 267 | } 268 | 269 | return false; 270 | } 271 | 272 | static void copyItems(DcmItem* src, DcmItem *dst) 273 | { 274 | // The source dataset is optional 275 | // 276 | if (!src) 277 | { 278 | return; 279 | } 280 | 281 | DcmObject* obj = nullptr; 282 | while (obj = src->nextInContainer(obj), obj != nullptr) 283 | { 284 | if (obj->getVR() == EVR_SQ) 285 | { 286 | // Ignore ReferencedFilmSessionSequence 287 | // 288 | continue; 289 | } 290 | 291 | // Insert with overwrite 292 | // 293 | dst->insert(dynamic_cast(obj->clone()), true); 294 | } 295 | } 296 | 297 | PrintSCP::PrintSCP(T_ASC_Association *assoc, QObject *parent, const QString &printer) 298 | : QObject(parent) 299 | , blockMode(DIMSE_BLOCKING) 300 | , timeout(DEFAULT_TIMEOUT) 301 | , forceUniqueSeries(false) 302 | , forceUniqueStudy(false) 303 | , sessionDataset(nullptr) 304 | , printer(printer) 305 | , upstreamNet(nullptr) 306 | , assoc(assoc) 307 | , upstream(nullptr) 308 | , debugUpstream(false) 309 | { 310 | QUtf8Settings settings; 311 | auto ocrLang = settings.value("ocr-lang", DEFAULT_OCR_LANG).toString(); 312 | 313 | #ifdef WITH_TESSERACT 314 | // Set locale to "C" to avoid tesseract crash. Then revert to the system default 315 | // 316 | auto oldLocale = setlocale(LC_NUMERIC, "C"); 317 | tess.Init(nullptr, ocrLang.toUtf8(), tesseract::OEM_TESSERACT_ONLY); 318 | setlocale(LC_NUMERIC, oldLocale); 319 | #endif 320 | 321 | blockMode = (T_DIMSE_BlockingMode)settings.value("block-mode", blockMode).toInt(); 322 | timeout = settings.value("timeout", timeout).toInt(); 323 | debugUpstream = settings.value("debug-upstream", debugUpstream).toBool(); 324 | reBadSymbols.setPattern(settings.value("bad-symbols").toString()); 325 | } 326 | 327 | PrintSCP::~PrintSCP() 328 | { 329 | dropAssociations(); 330 | ASC_dropNetwork(&upstreamNet); 331 | qDebug() << __func__ << "pid" << getpid(); 332 | } 333 | 334 | void PrintSCP::dump(const char* desc, DcmItem *dataset) 335 | { 336 | if (!dataset || !debugUpstream) 337 | return; 338 | 339 | std::stringstream ss; 340 | dataset->print(ss); 341 | qDebug() << desc << QString::fromLocal8Bit(ss.str().c_str()); 342 | } 343 | 344 | void PrintSCP::dumpIn(T_DIMSE_Message &msg, DcmItem *dataset) 345 | { 346 | if (!dataset || !debugUpstream) 347 | return; 348 | 349 | OFString str; 350 | DIMSE_dumpMessage(str, msg, DIMSE_INCOMING, dataset); 351 | qDebug() << QString::fromLocal8Bit(str.c_str()); 352 | } 353 | 354 | void PrintSCP::dumpOut(T_DIMSE_Message &msg, DcmItem *dataset) 355 | { 356 | if (!dataset || !debugUpstream) 357 | return; 358 | 359 | OFString str; 360 | DIMSE_dumpMessage(str, msg, DIMSE_OUTGOING, dataset); 361 | qDebug() << QString::fromLocal8Bit(str.c_str()); 362 | } 363 | 364 | bool PrintSCP::negotiateAssociation() 365 | { 366 | QUtf8Settings settings; 367 | char buf[BUFSIZ]; 368 | bool dropAssoc = false; 369 | 370 | const char *abstractSyntaxes[] = 371 | { 372 | UID_BasicGrayscalePrintManagementMetaSOPClass, 373 | UID_PresentationLUTSOPClass, 374 | UID_VerificationSOPClass, 375 | }; 376 | 377 | const char* transferSyntaxes[] = 378 | { 379 | #if __BYTE_ORDER == __LITTLE_ENDIAN 380 | UID_LittleEndianExplicitTransferSyntax, UID_BigEndianExplicitTransferSyntax, 381 | #elif __BYTE_ORDER == __BIG_ENDIAN 382 | UID_BigEndianExplicitTransferSyntax, UID_LittleEndianExplicitTransferSyntax, 383 | #else 384 | #error "Unsupported byte order" 385 | #endif 386 | UID_LittleEndianImplicitTransferSyntax 387 | }; 388 | 389 | printer = QString::fromUtf8(assoc->params->DULparams.calledAPTitle); 390 | 391 | qDebug() << "\n\n\nClient association received (max send PDV: " << assoc->sendPDVLength << ")" 392 | << assoc->params->DULparams.callingPresentationAddress << ":" 393 | << assoc->params->DULparams.callingAPTitle << "=>" 394 | << assoc->params->DULparams.calledPresentationAddress << ":" 395 | << assoc->params->DULparams.calledAPTitle 396 | << QDateTime::currentDateTime().toString(Qt::ISODate) 397 | ; 398 | 399 | ASC_setAPTitles(assoc->params, nullptr, nullptr, printer.toUtf8()); 400 | 401 | /* Application Context Name */ 402 | auto cond = ASC_getApplicationContextName(assoc->params, buf); 403 | if (cond.bad() || strcmp(buf, DICOM_STDAPPLICATIONCONTEXT) != 0) 404 | { 405 | /* reject: the application context name is not supported */ 406 | qDebug() << "Bad AppContextName: " << buf; 407 | cond = refuseAssociation(ASC_RESULT_REJECTEDTRANSIENT, ASC_REASON_SU_APPCONTEXTNAMENOTSUPPORTED); 408 | dropAssoc = true; 409 | } 410 | else if (!settings.childGroups().contains(printer)) 411 | { 412 | cond = refuseAssociation(ASC_RESULT_REJECTEDTRANSIENT, ASC_REASON_SU_CALLEDAETITLENOTRECOGNIZED); 413 | dropAssoc = true; 414 | } 415 | else 416 | { 417 | /* accept presentation contexts */ 418 | cond = ASC_acceptContextsWithPreferredTransferSyntaxes(assoc->params, 419 | abstractSyntaxes, sizeof(abstractSyntaxes)/sizeof(abstractSyntaxes[0]), 420 | transferSyntaxes, sizeof(transferSyntaxes)/sizeof(transferSyntaxes[0])); 421 | } 422 | 423 | if (dropAssoc) 424 | { 425 | printer.clear(); 426 | dropAssociations(); 427 | } 428 | else 429 | { 430 | // Initialize connection to upstream printer, if one is configured 431 | // 432 | settings.beginGroup(printer); 433 | auto printerAETitle = settings.value("upstream-aetitle").toString(); 434 | auto printerAddress = settings.value("upstream-address").toString(); 435 | auto calleeAETitle = settings.value("aetitle", assoc->params->DULparams.callingAPTitle).toString().toUpper(); 436 | forceUniqueSeries = settings.value("force-unique-series", forceUniqueSeries).toBool(); 437 | forceUniqueStudy = settings.value("force-unique-study", forceUniqueStudy).toBool(); 438 | debugUpstream = settings.value("debug-upstream", debugUpstream).toBool(); 439 | reBadSymbols.setPattern(settings.value("bad-symbols", reBadSymbols.pattern()).toString()); 440 | settings.endGroup(); 441 | 442 | if (printerAETitle.isEmpty()) 443 | { 444 | qDebug() << "No upstream connection for" << printer; 445 | } 446 | else 447 | { 448 | DIC_NODENAME localHost; 449 | T_ASC_Parameters* params = nullptr; 450 | 451 | auto port = settings.value("print-port", 0).toInt(); 452 | auto cond = ASC_initializeNetwork(NET_REQUESTOR, port, timeout, &upstreamNet); 453 | 454 | qDebug() << "Creating upstream connection to" << printer; 455 | 456 | cond = ASC_createAssociationParameters(¶ms, settings.value("pdu-size", ASC_DEFAULTMAXPDU).toInt()); 457 | if (cond.good()) 458 | { 459 | ASC_setAPTitles(params, calleeAETitle.toUtf8(), printerAETitle.toUtf8(), nullptr); 460 | 461 | // Figure out the presentation addresses and copy the 462 | // corresponding values into the DcmAssoc parameters. 463 | // 464 | gethostname(localHost, sizeof(localHost) - 1); 465 | ASC_setPresentationAddresses(params, localHost, printerAddress.toUtf8()); 466 | 467 | for (size_t i = 0; cond.good() && i < sizeof(abstractSyntaxes)/sizeof(abstractSyntaxes[0]); ++i) 468 | { 469 | cond = ASC_addPresentationContext(params, i*2+1, abstractSyntaxes[i], 470 | transferSyntaxes, sizeof(transferSyntaxes)/sizeof(transferSyntaxes[0])); 471 | } 472 | } 473 | 474 | if (cond.good()) 475 | { 476 | cond = ASC_requestAssociation(upstreamNet, params, &upstream); 477 | } 478 | 479 | if (cond.bad()) 480 | { 481 | qDebug() << "Failed to create association to" << printerAETitle << QString::fromLocal8Bit(cond.text()); 482 | ASC_destroyAssociation(&upstream); 483 | } 484 | else 485 | { 486 | // Dump general information concerning the establishment of the network connection if required 487 | // 488 | qDebug() << "Connection to upstream printer" << printer 489 | << "accepted (max send PDV: " << upstream->sendPDVLength << ")" 490 | << upstream->params->DULparams.callingPresentationAddress << ":" 491 | << upstream->params->DULparams.callingAPTitle << "=>" 492 | << upstream->params->DULparams.calledPresentationAddress << ":" 493 | << upstream->params->DULparams.calledAPTitle; 494 | } 495 | } 496 | 497 | // First of all, store the calee AE title. 498 | // Later we will add all attributes comes from client/server to the 499 | // final message. And store the message to the storage server. 500 | // 501 | sessionDataset = new DcmDataset; 502 | sessionDataset->putAndInsertString(DCM_RETIRED_DestinationAE, calleeAETitle.toUtf8()); 503 | 504 | // Fill in with some defaults 505 | // 506 | sessionDataset->putAndInsertString(DCM_PatientID, "0", false); 507 | sessionDataset->putAndInsertString(DCM_PatientName, "^", false); 508 | } 509 | 510 | return !dropAssoc; 511 | } 512 | 513 | OFCondition PrintSCP::refuseAssociation(T_ASC_RejectParametersResult result, T_ASC_RejectParametersReason reason) 514 | { 515 | qDebug() << __FUNCTION__ << result << reason; 516 | T_ASC_RejectParameters rej = { result, ASC_SOURCE_SERVICEUSER, reason }; 517 | 518 | void *associatePDU = nullptr; 519 | unsigned long associatePDUlength=0; 520 | OFCondition cond = ASC_rejectAssociation(assoc, &rej, &associatePDU, &associatePDUlength); 521 | delete[] (char *)associatePDU; 522 | return cond; 523 | } 524 | 525 | void PrintSCP::dropAssociations() 526 | { 527 | if (assoc) 528 | { 529 | if (assoc->params) 530 | { 531 | qDebug() << "Client association with" 532 | << assoc->params->DULparams.callingPresentationAddress << ":" 533 | << assoc->params->DULparams.callingAPTitle << "closed" << "pid" << getpid(); 534 | } 535 | else 536 | { 537 | qDebug() << "Client association with unknown params closed" << "pid" << getpid(); 538 | } 539 | ASC_dropSCPAssociation(assoc); 540 | ASC_destroyAssociation(&assoc); 541 | } 542 | 543 | if (upstream) 544 | { 545 | if (upstream->params) 546 | { 547 | qDebug() << "Upstream association with" 548 | << upstream->params->DULparams.callingPresentationAddress << ":" 549 | << upstream->params->DULparams.callingAPTitle << "closed" << "pid" << getpid(); 550 | } 551 | else 552 | { 553 | qDebug() << "Upstream association with unknown params closed" << "pid" << getpid(); 554 | } 555 | ASC_dropSCPAssociation(upstream); 556 | ASC_destroyAssociation(&upstream); 557 | ASC_dropNetwork(&upstreamNet); 558 | } 559 | 560 | delete sessionDataset; 561 | sessionDataset = nullptr; 562 | qDebug() << "Drop association completed. pid" << getpid(); 563 | } 564 | 565 | void PrintSCP::handleClient() 566 | { 567 | void *associatePDU = nullptr; 568 | unsigned long associatePDUlength = 0; 569 | 570 | OFCondition cond = ASC_acknowledgeAssociation(assoc, &associatePDU, &associatePDUlength); 571 | delete[] (char *)associatePDU; 572 | 573 | // Do the real work 574 | // 575 | while (cond.good()) 576 | { 577 | T_DIMSE_Message rq; 578 | T_DIMSE_Message rsp; 579 | T_ASC_PresentationContextID presId; 580 | T_ASC_PresentationContextID upstreamPresId = 0; 581 | DcmDataset *rawCommandSet = nullptr; 582 | DcmDataset *statusDetail = nullptr; 583 | DcmDataset *rqDataset = nullptr; 584 | DcmDataset *rspDataset = nullptr; 585 | 586 | cond = DIMSE_receiveCommand(assoc, DIMSE_BLOCKING, 0, &presId, &rq, &statusDetail, &rawCommandSet); 587 | 588 | if (cond.bad()) 589 | { 590 | qDebug() << "DIMSE_receiveCommand" << QString::fromLocal8Bit(cond.text()); 591 | break; 592 | } 593 | 594 | dump("statusDetail", statusDetail); 595 | dump("rawCommandSet", rawCommandSet); 596 | delete rawCommandSet; 597 | rawCommandSet = nullptr; 598 | 599 | if (isDatasetPresent(rq)) 600 | { 601 | cond = DIMSE_receiveDataSetInMemory(assoc, blockMode, timeout, &presId, &rqDataset, nullptr, nullptr); 602 | if (cond.bad()) 603 | { 604 | qDebug() << "DIMSE_receiveDataSetInMemory" << QString::fromLocal8Bit(cond.text()); 605 | break; 606 | } 607 | } 608 | 609 | dumpIn(rq, rqDataset); 610 | 611 | if (upstream) 612 | { 613 | cond = DIMSE_sendMessageUsingMemoryData(upstream, presId, &rq, statusDetail, rqDataset, nullptr, nullptr, &rawCommandSet); 614 | dump("rawCommandSet", rawCommandSet); 615 | delete rawCommandSet; 616 | rawCommandSet = nullptr; 617 | delete statusDetail; 618 | statusDetail = nullptr; 619 | 620 | if (cond.bad()) 621 | { 622 | qDebug() << "DIMSE_sendMessageUsingMemoryData(upstream) failed" << QString::fromLocal8Bit(cond.text()) 623 | << "presId" << presId; 624 | break; 625 | } 626 | 627 | cond = DIMSE_receiveCommand(upstream, blockMode, timeout, &upstreamPresId, &rsp, &statusDetail, &rawCommandSet); 628 | dump("rawCommandSet", rawCommandSet); 629 | delete rawCommandSet; 630 | rawCommandSet = nullptr; 631 | dump("statusDetail", statusDetail); 632 | 633 | if (cond.bad()) 634 | { 635 | qDebug() << "DIMSE_recv(upstream) failed" << QString::fromLocal8Bit(cond.text()); 636 | break; 637 | } 638 | 639 | if (rq.CommandField != (rsp.CommandField & ~0x8000)) 640 | { 641 | qDebug() << "Mismatched response: rq" << rq.CommandField << "rsp" << rsp.CommandField; 642 | } 643 | 644 | if (isDatasetPresent(rsp)) 645 | { 646 | cond = DIMSE_receiveDataSetInMemory(upstream, blockMode, timeout, &upstreamPresId, &rspDataset, nullptr, nullptr); 647 | if (cond.bad()) 648 | { 649 | qDebug() << "DIMSE_receiveDataSetInMemory(upstream)" << QString::fromLocal8Bit(cond.text()); 650 | break; 651 | } 652 | } 653 | } 654 | else 655 | { 656 | /* process command */ 657 | switch (rq.CommandField) 658 | { 659 | case DIMSE_C_ECHO_RQ: 660 | cond = handleCEcho(rq, rqDataset, rsp, rspDataset); 661 | break; 662 | case DIMSE_N_GET_RQ: 663 | cond = handleNGet(rq, rqDataset, rsp, rspDataset); 664 | break; 665 | case DIMSE_N_SET_RQ: 666 | cond = handleNSet(rq, rqDataset, rsp, rspDataset); 667 | break; 668 | case DIMSE_N_ACTION_RQ: 669 | cond = handleNAction(rq, rqDataset, rsp, rspDataset); 670 | break; 671 | case DIMSE_N_CREATE_RQ: 672 | cond = handleNCreate(rq, rqDataset, rsp, rspDataset); 673 | break; 674 | case DIMSE_N_DELETE_RQ: 675 | cond = handleNDelete(rq, rqDataset, rsp, rspDataset); 676 | break; 677 | default: 678 | cond = DIMSE_BADCOMMANDTYPE; /* unsupported command */ 679 | qDebug() << "Cannot handle command: 0x" << QString::number((unsigned)rq.CommandField, 16); 680 | break; 681 | } 682 | } 683 | 684 | if (DIMSE_N_SET_RQ == rq.CommandField 685 | && QString(rq.msg.NSetRQ.RequestedSOPClassUID).startsWith(UID_BasicGrayscaleImageBoxSOPClass)) 686 | { 687 | SOPInstanceUID = QString::fromUtf8(rq.msg.NSetRQ.RequestedSOPInstanceUID); 688 | char uid[100] = {0}; 689 | 690 | if (forceUniqueStudy) 691 | { 692 | studyInstanceUID = QString::fromUtf8(dcmGenerateUniqueIdentifier(uid, SITE_STUDY_UID_ROOT)); 693 | } 694 | 695 | if (forceUniqueSeries) 696 | { 697 | seriesInstanceUID = QString::fromUtf8(dcmGenerateUniqueIdentifier(uid, SITE_SERIES_UID_ROOT)); 698 | } 699 | 700 | storeImage(rqDataset); 701 | } 702 | else 703 | { 704 | if (DIMSE_N_CREATE_RQ == rq.CommandField) 705 | { 706 | if (0 == strcmp(rq.msg.NCreateRQ.AffectedSOPClassUID, UID_BasicFilmSessionSOPClass)) 707 | { 708 | studyInstanceUID = QString::fromUtf8(rsp.msg.NCreateRSP.AffectedSOPInstanceUID); 709 | } 710 | else if (0 == strcmp(rq.msg.NCreateRQ.AffectedSOPClassUID, UID_BasicFilmBoxSOPClass)) 711 | { 712 | seriesInstanceUID = QString::fromUtf8(rsp.msg.NCreateRSP.AffectedSOPInstanceUID); 713 | } 714 | } 715 | 716 | copyItems(rqDataset, sessionDataset); 717 | copyItems(rspDataset, sessionDataset); 718 | } 719 | 720 | delete rqDataset; 721 | rqDataset = nullptr; 722 | 723 | dumpOut(rsp, rspDataset); 724 | cond = DIMSE_sendMessageUsingMemoryData(assoc, presId, &rsp, statusDetail, rspDataset, nullptr, nullptr, &rawCommandSet); 725 | dump("rawCommandSet", rawCommandSet); 726 | delete rawCommandSet; 727 | rawCommandSet = nullptr; 728 | delete statusDetail; 729 | statusDetail = nullptr; 730 | delete rspDataset; 731 | rspDataset = nullptr; 732 | 733 | if (cond.bad()) 734 | { 735 | qDebug() << "DIMSE_sendMessageUsingMemoryData" << QString::fromLocal8Bit(cond.text()); 736 | break; 737 | } 738 | 739 | } /* while */ 740 | 741 | qDebug() << "Print session is done"; 742 | 743 | // Close client association 744 | // 745 | if (cond == DUL_PEERREQUESTEDRELEASE) 746 | { 747 | qDebug() << "Association Release"; 748 | cond = ASC_acknowledgeRelease(assoc); 749 | } 750 | else if (cond == DUL_PEERABORTEDASSOCIATION) 751 | { 752 | qDebug() << "Association Aborted" << (assoc->params? assoc->params->DULparams.callingPresentationAddress: ""); 753 | } 754 | else 755 | { 756 | qDebug() << "DIMSE Failure (aborting association)" << (assoc->params? assoc->params->DULparams.callingPresentationAddress: ""); 757 | cond = ASC_abortAssociation(assoc); 758 | } 759 | 760 | // close upstream printer association 761 | // 762 | if (upstream) 763 | { 764 | ASC_releaseAssociation(upstream); 765 | } 766 | 767 | dropAssociations(); 768 | } 769 | 770 | OFCondition PrintSCP::handleCEcho(T_DIMSE_Message& rq, DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *&) 771 | { 772 | rsp.CommandField = DIMSE_C_ECHO_RSP; 773 | rsp.msg.CEchoRSP.MessageIDBeingRespondedTo = rq.msg.CEchoRQ.MessageID; 774 | rsp.msg.CEchoRSP.AffectedSOPClassUID[0] = 0; 775 | rsp.msg.CEchoRSP.DataSetType = DIMSE_DATASET_NULL; 776 | rsp.msg.CEchoRSP.DimseStatus = STATUS_Success; 777 | rsp.msg.CEchoRSP.opts = 0; 778 | 779 | OFCondition cond = EC_Normal; 780 | 781 | return cond; 782 | } 783 | 784 | OFCondition PrintSCP::handleNGet(T_DIMSE_Message& rq, DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *& rspDataset) 785 | { 786 | // initialize response message 787 | rsp.CommandField = DIMSE_N_GET_RSP; 788 | rsp.msg.NGetRSP.MessageIDBeingRespondedTo = rq.msg.NGetRQ.MessageID; 789 | rsp.msg.NGetRSP.AffectedSOPClassUID[0] = 0; 790 | rsp.msg.NGetRSP.DimseStatus = STATUS_Success; 791 | rsp.msg.NGetRSP.AffectedSOPInstanceUID[0] = 0; 792 | rsp.msg.NGetRSP.DataSetType = DIMSE_DATASET_NULL; 793 | rsp.msg.NGetRSP.opts = 0; 794 | 795 | OFCondition cond = EC_Normal; 796 | 797 | QString sopClass(rq.msg.NGetRQ.RequestedSOPClassUID); 798 | if (sopClass == UID_PrinterSOPClass) 799 | { 800 | // Print N-GET 801 | printerNGet(rq, rsp, rspDataset); 802 | } 803 | else 804 | { 805 | qDebug() << "N-GET unsupported for SOP class '" << sopClass << "'"; 806 | rsp.msg.NGetRSP.DimseStatus = STATUS_N_NoSuchSOPClass; 807 | } 808 | 809 | return cond; 810 | } 811 | 812 | OFCondition PrintSCP::handleNSet(T_DIMSE_Message& rq, DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *&) 813 | { 814 | // initialize response message 815 | rsp.CommandField = DIMSE_N_SET_RSP; 816 | rsp.msg.NSetRSP.MessageIDBeingRespondedTo = rq.msg.NSetRQ.MessageID; 817 | rsp.msg.NSetRSP.AffectedSOPClassUID[0] = 0; 818 | rsp.msg.NSetRSP.DimseStatus = STATUS_Success; 819 | rsp.msg.NSetRSP.AffectedSOPInstanceUID[0] = 0; 820 | rsp.msg.NSetRSP.DataSetType = DIMSE_DATASET_NULL; 821 | rsp.msg.NSetRSP.opts = 0; 822 | 823 | OFCondition cond = EC_Normal; 824 | 825 | return cond; 826 | } 827 | 828 | 829 | OFCondition PrintSCP::handleNAction(T_DIMSE_Message& rq, DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *&) 830 | { 831 | // initialize response message 832 | rsp.CommandField = DIMSE_N_ACTION_RSP; 833 | rsp.msg.NActionRSP.MessageIDBeingRespondedTo = rq.msg.NActionRQ.MessageID; 834 | rsp.msg.NActionRSP.AffectedSOPClassUID[0] = 0; 835 | rsp.msg.NActionRSP.DimseStatus = STATUS_Success; 836 | rsp.msg.NActionRSP.AffectedSOPInstanceUID[0] = 0; 837 | rsp.msg.NActionRSP.ActionTypeID = rq.msg.NActionRQ.ActionTypeID; 838 | rsp.msg.NActionRSP.DataSetType = DIMSE_DATASET_NULL; 839 | rsp.msg.NActionRSP.opts = O_NACTION_ACTIONTYPEID; 840 | 841 | OFCondition cond = EC_Normal; 842 | 843 | return cond; 844 | } 845 | 846 | OFCondition PrintSCP::handleNCreate(T_DIMSE_Message& rq, DcmDataset *rqDataset, T_DIMSE_Message& rsp, DcmDataset *& rspDataset) 847 | { 848 | // initialize response message 849 | rsp.CommandField = DIMSE_N_CREATE_RSP; 850 | rsp.msg.NCreateRSP.MessageIDBeingRespondedTo = rq.msg.NCreateRQ.MessageID; 851 | rsp.msg.NCreateRSP.AffectedSOPClassUID[0] = 0; 852 | rsp.msg.NCreateRSP.DimseStatus = STATUS_Success; 853 | if (rq.msg.NCreateRQ.opts & O_NCREATE_AFFECTEDSOPINSTANCEUID) 854 | { 855 | // instance UID is provided by SCU 856 | strncpy(rsp.msg.NCreateRSP.AffectedSOPInstanceUID, rq.msg.NCreateRQ.AffectedSOPInstanceUID, sizeof(DIC_UI)); 857 | } 858 | else 859 | { 860 | // we generate our own instance UID 861 | dcmGenerateUniqueIdentifier(rsp.msg.NCreateRSP.AffectedSOPInstanceUID); 862 | } 863 | rsp.msg.NCreateRSP.DataSetType = DIMSE_DATASET_NULL; 864 | rsp.msg.NCreateRSP.opts = O_NCREATE_AFFECTEDSOPINSTANCEUID | O_NCREATE_AFFECTEDSOPCLASSUID; 865 | strncpy(rsp.msg.NCreateRSP.AffectedSOPClassUID, rq.msg.NCreateRQ.AffectedSOPClassUID, sizeof(DIC_UI)); 866 | 867 | OFCondition cond = EC_Normal; 868 | 869 | QString sopClass(rq.msg.NCreateRQ.AffectedSOPClassUID); 870 | if (sopClass == UID_BasicFilmSessionSOPClass) 871 | { 872 | // BFS N-CREATE 873 | filmSessionNCreate(rqDataset, rsp, rspDataset); 874 | } 875 | else if (sopClass == UID_BasicFilmBoxSOPClass) 876 | { 877 | // BFB N-CREATE 878 | filmBoxNCreate(rqDataset, rsp, rspDataset); 879 | } 880 | else if (sopClass == UID_PresentationLUTSOPClass) 881 | { 882 | // P-LUT N-CREATE 883 | presentationLUTNCreate(rqDataset, rsp, rspDataset); 884 | } 885 | else 886 | { 887 | qDebug() << "N-CREATE unsupported for SOP class '" << sopClass << "'"; 888 | rsp.msg.NCreateRSP.DimseStatus = STATUS_N_NoSuchSOPClass; 889 | rsp.msg.NCreateRSP.opts = 0; // don't include affected SOP instance UID 890 | } 891 | 892 | return cond; 893 | } 894 | 895 | OFCondition PrintSCP::handleNDelete(T_DIMSE_Message& rq, DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *&) 896 | { 897 | // initialize response message 898 | rsp.CommandField = DIMSE_N_DELETE_RSP; 899 | rsp.msg.NDeleteRSP.MessageIDBeingRespondedTo = rq.msg.NDeleteRQ.MessageID; 900 | rsp.msg.NDeleteRSP.AffectedSOPClassUID[0] = 0; 901 | rsp.msg.NDeleteRSP.DimseStatus = STATUS_Success; 902 | rsp.msg.NDeleteRSP.AffectedSOPInstanceUID[0] = 0; 903 | rsp.msg.NDeleteRSP.DataSetType = DIMSE_DATASET_NULL; 904 | rsp.msg.NDeleteRSP.opts = 0; 905 | 906 | OFCondition cond = EC_Normal; 907 | 908 | QString sopClass(rq.msg.NDeleteRQ.RequestedSOPClassUID); 909 | if (sopClass == UID_BasicFilmSessionSOPClass) 910 | { 911 | // BFS N-DELETE 912 | filmSessionNDelete(rq, rsp); 913 | } 914 | else if (sopClass == UID_BasicFilmBoxSOPClass) 915 | { 916 | // BFB N-DELETE 917 | filmBoxNDelete(rq, rsp); 918 | } 919 | else if (sopClass == UID_PresentationLUTSOPClass) 920 | { 921 | // P-LUT N-DELETE 922 | presentationLUTNDelete(rq, rsp); 923 | } 924 | else 925 | { 926 | qDebug() << "N-DELETE unsupported for SOP class '" << sopClass << "'"; 927 | rsp.msg.NDeleteRSP.DimseStatus = STATUS_N_NoSuchSOPClass; 928 | } 929 | 930 | return cond; 931 | } 932 | 933 | void PrintSCP::printerNGet(T_DIMSE_Message& rq, T_DIMSE_Message& rsp, DcmDataset *& rspDataset) 934 | { 935 | QString printerInstance(UID_PrinterSOPInstance); 936 | if (printerInstance == rq.msg.NGetRQ.RequestedSOPInstanceUID) 937 | { 938 | rsp.msg.NSetRSP.DataSetType = DIMSE_DATASET_PRESENT; 939 | rspDataset = new DcmDataset; 940 | 941 | // By default, send only PrinterStatus & PrinterStatusInfo 942 | // 943 | if (rq.msg.NGetRQ.ListCount == 0) 944 | { 945 | rspDataset->putAndInsertString(DCM_PrinterStatus, DEFAULT_printerStatus); 946 | rspDataset->putAndInsertString(DCM_PrinterStatusInfo, DEFAULT_printerStatusInfo); 947 | } 948 | else 949 | { 950 | QUtf8Settings settings; 951 | settings.beginGroup(printer); 952 | QMap info; 953 | auto size = settings.beginReadArray("info"); 954 | for (int idx = 0; idx < size; ++idx) 955 | { 956 | settings.setArrayIndex(idx); 957 | auto key = settings.value("key").toString(); 958 | DcmTag tag; 959 | if (DcmTag::findTagFromName(key.toUtf8(), tag).good()) 960 | { 961 | info[tag] = settings.value("value"); 962 | } 963 | else 964 | { 965 | qDebug() << "Bad DICOM tag" << key << "in" << printer << "info" << idx; 966 | } 967 | } 968 | settings.endArray(); 969 | settings.endGroup(); 970 | 971 | for (int i = 0; i < rq.msg.NGetRQ.ListCount / 2; ++i) 972 | { 973 | auto group = rq.msg.NGetRQ.AttributeIdentifierList[i*2]; 974 | auto element = rq.msg.NGetRQ.AttributeIdentifierList[i*2 + 1]; 975 | if (element == 0x0000) 976 | { 977 | // Group length 978 | // 979 | continue; 980 | } 981 | 982 | if (group == DCM_PrinterStatus.getGroup()) 983 | { 984 | if (element == DCM_PrinterStatus.getElement()) 985 | { 986 | rspDataset->putAndInsertString(DCM_PrinterStatus, DEFAULT_printerStatus); 987 | continue; 988 | } 989 | if (element == DCM_PrinterStatusInfo.getElement()) 990 | { 991 | rspDataset->putAndInsertString(DCM_PrinterStatusInfo, DEFAULT_printerStatusInfo); 992 | continue; 993 | } 994 | } 995 | 996 | // Some unknown element was requested. 997 | // 998 | DcmTag tag(group, element); 999 | if (!info.contains(tag)) 1000 | { 1001 | qDebug() << "cannot retrieve printer information: unknown attribute (" 1002 | << QString::number(group, 16) << "," << QString::number(element, 16) 1003 | << ") in attribute list."; 1004 | rsp.msg.NGetRSP.DimseStatus = STATUS_N_NoSuchAttribute; 1005 | delete rspDataset; 1006 | rspDataset = nullptr; 1007 | break; 1008 | } 1009 | 1010 | putAndInsertVariant(rspDataset, tag, info[tag]); 1011 | } 1012 | } 1013 | } 1014 | else 1015 | { 1016 | qDebug() << "cannot retrieve printer information, unknown printer SOP instance UID" << printerInstance; 1017 | rsp.msg.NGetRSP.DimseStatus = STATUS_N_NoSuchObjectInstance; 1018 | } 1019 | } 1020 | 1021 | void PrintSCP::filmSessionNCreate(DcmDataset *, T_DIMSE_Message& rsp, DcmDataset *&) 1022 | { 1023 | if (!studyInstanceUID.isEmpty()) 1024 | { 1025 | // film session exists already, refuse n-create 1026 | qDebug() << "cannot create two film sessions concurrently."; 1027 | rsp.msg.NCreateRSP.DimseStatus = STATUS_N_DuplicateSOPInstance; 1028 | rsp.msg.NCreateRSP.opts = 0; // don't include affected SOP instance UID 1029 | } 1030 | } 1031 | 1032 | void PrintSCP::filmBoxNCreate(DcmDataset *rqDataset, T_DIMSE_Message& rsp, DcmDataset *& rspDataset) 1033 | { 1034 | rsp.msg.NCreateRSP.DataSetType = DIMSE_DATASET_PRESENT; 1035 | rspDataset = rqDataset? new DcmDataset(*rqDataset): new DcmDataset; 1036 | auto dseq = new DcmSequenceOfItems(DCM_ReferencedImageBoxSequence); 1037 | char uid[100]; 1038 | OFString fmt; 1039 | unsigned long count = 1; 1040 | 1041 | if (rspDataset->findAndGetOFStringArray(DCM_ImageDisplayFormat, fmt).good() && fmt.substr(0,9) == "STANDARD\\") 1042 | { 1043 | unsigned long rows = 0; 1044 | unsigned long cols = 0; 1045 | if (2 == sscanf(fmt.c_str() + 9, "%lu,%lu", &cols, &rows)) 1046 | { 1047 | count = rows * cols; 1048 | } 1049 | } 1050 | 1051 | while (count-- > 0) 1052 | { 1053 | auto ditem = new DcmItem(); 1054 | ditem->putAndInsertString(DCM_ReferencedSOPClassUID, UID_BasicGrayscaleImageBoxSOPClass); 1055 | ditem->putAndInsertString(DCM_ReferencedSOPInstanceUID, dcmGenerateUniqueIdentifier(uid, SITE_INSTANCE_UID_ROOT)); 1056 | dseq->insert(ditem); 1057 | } 1058 | 1059 | rspDataset->insert(dseq); 1060 | } 1061 | 1062 | void PrintSCP::presentationLUTNCreate(DcmDataset *rqDataset, T_DIMSE_Message& rsp, DcmDataset *& rspDataset) 1063 | { 1064 | if (rqDataset) 1065 | { 1066 | rsp.msg.NCreateRSP.DataSetType = DIMSE_DATASET_PRESENT; 1067 | rspDataset = (DcmDataset*)rqDataset->clone(); 1068 | } 1069 | } 1070 | 1071 | void PrintSCP::filmSessionNDelete(T_DIMSE_Message&, T_DIMSE_Message&) 1072 | { 1073 | studyInstanceUID.clear(); 1074 | SOPInstanceUID.clear(); 1075 | seriesInstanceUID.clear(); 1076 | } 1077 | 1078 | void PrintSCP::filmBoxNDelete(T_DIMSE_Message&, T_DIMSE_Message&) 1079 | { 1080 | } 1081 | 1082 | void PrintSCP::presentationLUTNDelete(T_DIMSE_Message&, T_DIMSE_Message&) 1083 | { 1084 | } 1085 | 1086 | void PrintSCP::storeImage(DcmDataset *rqDataset) 1087 | { 1088 | if (!rqDataset) 1089 | { 1090 | qDebug() << __FUNCTION__ << "Request dataset is missing"; 1091 | return; 1092 | } 1093 | 1094 | DcmItem *item = nullptr; 1095 | auto cond = rqDataset->findAndGetSequenceItem(DCM_BasicGrayscaleImageSequence, item); 1096 | if (cond.good()) 1097 | { 1098 | // Pull up children items from sequence to the dataset 1099 | // 1100 | DcmElement* obj = nullptr; 1101 | while (obj = item->remove(0UL), obj != nullptr) 1102 | { 1103 | rqDataset->insert(obj); 1104 | } 1105 | rqDataset->remove(DCM_BasicGrayscaleImageSequence); 1106 | delete item; 1107 | } 1108 | 1109 | copyItems(sessionDataset, rqDataset); 1110 | 1111 | rqDataset->putAndInsertString(DCM_SpecificCharacterSet, "ISO_IR 192"); // UTF-8 1112 | 1113 | rqDataset->putAndInsertString(DCM_StudyInstanceUID, studyInstanceUID.toUtf8()); 1114 | rqDataset->putAndInsertString(DCM_SeriesInstanceUID, seriesInstanceUID.toUtf8()); 1115 | rqDataset->putAndInsertString(DCM_SOPInstanceUID, SOPInstanceUID.toUtf8()); 1116 | 1117 | auto now = QDateTime::currentDateTime(); 1118 | putAndInsertVariant(rqDataset, DCM_InstanceCreationDate, now); 1119 | putAndInsertVariant(rqDataset, DCM_InstanceCreationTime, now); 1120 | putAndInsertVariant(rqDataset, DCM_StudyDate, now); 1121 | putAndInsertVariant(rqDataset, DCM_StudyTime, now); 1122 | 1123 | rqDataset->putAndInsertString(DCM_Manufacturer, ORGANIZATION_FULL_NAME); 1124 | rqDataset->putAndInsertString(DCM_ManufacturerModelName, PRODUCT_FULL_NAME); 1125 | 1126 | QUtf8Settings settings; 1127 | auto spoolPath = settings.value("spool-path").toString(); 1128 | 1129 | if (!webQuery(rqDataset)) 1130 | { 1131 | if (!spoolPath.isEmpty()) 1132 | { 1133 | rqDataset->putAndInsertString(DCM_RETIRED_PrintQueueID, printer.toUtf8()); 1134 | saveToDisk(spoolPath, rqDataset); 1135 | } 1136 | } 1137 | else 1138 | { 1139 | if (settings.value("debug").toInt() > 1) 1140 | { 1141 | saveToDisk(".", rqDataset); 1142 | } 1143 | 1144 | foreach (auto server, settings.value("storage-servers").toStringList()) 1145 | { 1146 | StoreSCP sscp(server); 1147 | cond = sscp.sendToServer(rqDataset, SOPInstanceUID.toUtf8()); 1148 | if (cond.bad()) 1149 | { 1150 | qDebug() << "Failed to store to" << server << QString::fromLocal8Bit(cond.text()); 1151 | if (!spoolPath.isEmpty()) 1152 | { 1153 | saveToDisk(QString(spoolPath).append(QDir::separator()).append(server), rqDataset); 1154 | } 1155 | } 1156 | } 1157 | } 1158 | } 1159 | 1160 | static QByteArray writeXmlRequest(const QString& root, const QVariantMap& map) 1161 | { 1162 | QByteArray data; 1163 | QXmlStreamWriter xml(&data); 1164 | 1165 | xml.writeStartDocument(); 1166 | xml.writeStartElement(root); 1167 | for (auto i = map.constBegin(); i != map.constEnd(); ++i) 1168 | { 1169 | xml.writeTextElement(i.key(), i.value().toString()); 1170 | } 1171 | xml.writeEndElement(); 1172 | xml.writeEndDocument(); 1173 | 1174 | return data; 1175 | } 1176 | 1177 | static QVariantMap readXmlResponse(const QByteArray& data) 1178 | { 1179 | QXmlStreamReader xml(data); 1180 | QVariantMap map; 1181 | 1182 | while (xml.readNextStartElement()) 1183 | { 1184 | if (xml.name() == "element") 1185 | { 1186 | auto key = xml.attributes().value("tag").toString(); 1187 | map[key] = xml.readElementText(); 1188 | } 1189 | else if (xml.name() != "data-set" && xml.name() != "business-logic-error") 1190 | { 1191 | auto text = xml.readElementText(QXmlStreamReader::SkipChildElements); 1192 | if (text.isEmpty()) 1193 | { 1194 | qDebug() << "Unexpected element" << xml.name(); 1195 | } 1196 | else 1197 | { 1198 | map[xml.name().toString()] = text; 1199 | } 1200 | } 1201 | } 1202 | 1203 | return map; 1204 | } 1205 | 1206 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 1207 | static QVariantMap readJsonResponse(const QByteArray& data) 1208 | { 1209 | QVariantMap map; 1210 | auto elements = QJsonDocument::fromJson(data).array(); 1211 | qDebug() << "Server response is about" << elements.size() << "elements"; 1212 | 1213 | Q_FOREACH (auto elm, elements) 1214 | { 1215 | auto obj = elm.toObject(); 1216 | map[obj.value("tag").toString()] = obj.value("value"); 1217 | } 1218 | 1219 | return map; 1220 | } 1221 | #endif 1222 | 1223 | bool PrintSCP::webQuery(DcmDataset *rqDataset) 1224 | { 1225 | QUtf8Settings settings; 1226 | QVariantMap queryParams; 1227 | QVariantMap ret; 1228 | 1229 | settings.beginGroup("query"); 1230 | auto url = settings.value("url").toUrl(); 1231 | auto userName = settings.value("username").toString(); 1232 | auto password = settings.value("password").toString(); 1233 | auto contendType = settings.value("content-type", DEFAULT_CONTENT_TYPE).toString(); 1234 | 1235 | QStringList extraParams; 1236 | if (contendType.contains("/xml", Qt::CaseInsensitive)) 1237 | { 1238 | extraParams.append("study-instance-uid:StudyInstanceUID"); 1239 | extraParams.append("medical-service-date:InstanceCreationDate"); 1240 | } 1241 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 1242 | else if (contendType.contains("/json", Qt::CaseInsensitive)) 1243 | { 1244 | extraParams.append("studyInstanceUID:StudyInstanceUID"); 1245 | extraParams.append("medicalServiceDate:InstanceCreationDate"); 1246 | } 1247 | #endif 1248 | 1249 | extraParams = settings.value("query-parameters", extraParams).toStringList(); 1250 | auto ignoreErrors = settings.value("ignore-errors").toStringList(); 1251 | settings.endGroup(); 1252 | 1253 | settings.beginGroup(printer); 1254 | settings.beginGroup("query"); 1255 | url = settings.value("url", url).toUrl(); 1256 | userName = settings.value("username", userName).toString(); 1257 | password = settings.value("password", password).toString(); 1258 | contendType = settings.value("content-type", contendType).toString(); 1259 | extraParams = settings.value("query-parameters", extraParams).toStringList(); 1260 | ignoreErrors = settings.value("ignore-errors", ignoreErrors).toStringList(); 1261 | settings.endGroup(); 1262 | settings.endGroup(); 1263 | 1264 | if (url.isEmpty()) 1265 | { 1266 | return true; 1267 | } 1268 | 1269 | DicomImage di(rqDataset, rqDataset->getOriginalXfer()); 1270 | void *img = nullptr; 1271 | 1272 | if (di.createJavaAWTBitmap(img, 0, 32) && img) 1273 | { 1274 | #ifdef WITH_TESSERACT 1275 | tess.SetImage((const unsigned char*)img, di.getWidth(), di.getHeight(), 4, 4 * di.getWidth()); 1276 | #endif 1277 | // Global tags 1278 | // 1279 | insertTags(rqDataset, queryParams, &di, settings); 1280 | 1281 | // This printer tags 1282 | // 1283 | settings.beginGroup(printer); 1284 | insertTags(rqDataset, queryParams, &di, settings); 1285 | settings.endGroup(); 1286 | delete[] (Uint32*)img; 1287 | } 1288 | 1289 | Q_FOREACH (auto extraParam, extraParams) 1290 | { 1291 | auto parts = extraParam.split(QRegExp("=|:")); 1292 | QVariant value; 1293 | 1294 | if (parts.size() > 1) 1295 | { 1296 | DcmTag tag; 1297 | if (DcmTag::findTagFromName(parts[1].toUtf8(), tag).bad()) 1298 | { 1299 | qDebug() << "Unknown DCM tag" << parts[1]; 1300 | } 1301 | else if (findAndGetVariant(rqDataset, tag, value).bad()) 1302 | { 1303 | qDebug() << "Failed te retrieve DCM tag" << tag.getTagName() << "from the dataset"; 1304 | } 1305 | } 1306 | queryParams[parts[0]] = value; 1307 | } 1308 | 1309 | bool error = false; 1310 | QNetworkAccessManager mgr; 1311 | 1312 | QByteArray data; 1313 | if (contendType.contains("/xml", Qt::CaseInsensitive)) 1314 | { 1315 | data = writeXmlRequest("save-hardcopy-grayscale-image-request", queryParams); 1316 | } 1317 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 1318 | else if (contendType.contains("/json", Qt::CaseInsensitive)) 1319 | { 1320 | data = QJsonDocument(QJsonObject::fromVariantMap(queryParams)) 1321 | .toJson( 1322 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 1, 0)) 1323 | QJsonDocument::Compact 1324 | #endif 1325 | ); 1326 | } 1327 | #endif 1328 | else 1329 | { 1330 | qDebug() << contendType << "not supported"; 1331 | } 1332 | 1333 | QNetworkRequest rq(url); 1334 | rq.setRawHeader("Accept", "*"); 1335 | if (!userName.isEmpty()) 1336 | { 1337 | rq.setRawHeader("Authorization", "Basic " + QByteArray(userName.append(':').append(password).toUtf8()).toBase64()); 1338 | } 1339 | 1340 | // Enforce the UTF-8 charset if no charsets are specified. 1341 | // Note that all requests in the JSON format must use UTF-8 charset. 1342 | // 1343 | if (!contendType.contains("charset=", Qt::CaseInsensitive)) 1344 | { 1345 | contendType.append("; charset=").append(DEFAULT_CHARSET); 1346 | } 1347 | 1348 | rq.setHeader(QNetworkRequest::ContentTypeHeader, contendType); 1349 | rq.setHeader(QNetworkRequest::ContentLengthHeader, data.size()); 1350 | qDebug() << url << data; 1351 | auto reply = mgr.post(rq, data); 1352 | auto start = QDateTime::currentMSecsSinceEpoch(); 1353 | 1354 | while (reply->isRunning() && (timeout <= 0 || timeout > (QDateTime::currentMSecsSinceEpoch() - start) / 1000)) 1355 | { 1356 | qApp->processEvents(QEventLoop::AllEvents, 100); 1357 | } 1358 | 1359 | if (reply->isRunning()) 1360 | { 1361 | qDebug() << "Web query request timeout, aborting"; 1362 | reply->abort(); 1363 | qApp->processEvents(QEventLoop::AllEvents, 100); 1364 | ++error; 1365 | } 1366 | 1367 | auto responseContentType = reply->header(QNetworkRequest::ContentTypeHeader).toString(); 1368 | auto response = reply->readAll(); 1369 | 1370 | if (settings.value("debug").toBool()) 1371 | { 1372 | qDebug() << reply->error() << reply->errorString() 1373 | << responseContentType << QString::fromUtf8(response); 1374 | } 1375 | 1376 | if (reply->error()) 1377 | { 1378 | ++error; 1379 | } 1380 | 1381 | if (responseContentType.contains("/xml")) 1382 | { 1383 | ret = readXmlResponse(response); 1384 | } 1385 | #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 1386 | else if (responseContentType.contains("/json")) 1387 | { 1388 | ret = readJsonResponse(response); 1389 | } 1390 | #endif 1391 | else 1392 | { 1393 | qDebug() << "response content type" << responseContentType << "not supported"; 1394 | error = true; 1395 | } 1396 | 1397 | // Check for errors we can safelly ignore 1398 | // 1399 | if (error) 1400 | { 1401 | auto msg = ret.contains("message")? ret["message"].toString(): QString::fromUtf8(response); 1402 | 1403 | Q_FOREACH (auto ignore, ignoreErrors) 1404 | { 1405 | if (msg.contains(ignore)) 1406 | { 1407 | error = false; 1408 | qDebug() << ignore << "found in the response. The error was suppressed"; 1409 | break; 1410 | } 1411 | } 1412 | } 1413 | 1414 | // Add some required fields, in case if they are empty. 1415 | // Normally, we expect them to be overriden with the data received from the app server. 1416 | // Also, reset the patient name & id in case of an error. 1417 | // 1418 | rqDataset->putAndInsertString(DCM_PatientID, "0", error); 1419 | rqDataset->putAndInsertString(DCM_PatientName, "^", error); 1420 | 1421 | if (!error) 1422 | { 1423 | // Store web service response to the dataset. 1424 | // All values must be serialized to strings in the DICOM way, 1425 | // i.e. '20141225' for date values, '175959' for time values. 1426 | // 1427 | for (auto i = ret.constBegin(); i != ret.constEnd(); ++i) 1428 | { 1429 | DcmTag tag; 1430 | if (DcmTag::findTagFromName(i.key().toUtf8(), tag).bad()) 1431 | { 1432 | qDebug() << "Unknown DCM tag" << i.key(); 1433 | continue; 1434 | } 1435 | 1436 | QVariant value; 1437 | auto str = i.value().toString(); 1438 | 1439 | // We shouldn't call translateToLatin for integers & dates. 1440 | // 1441 | if (tag.getEVR() == EVR_DA && str.length() == 8) 1442 | { 1443 | value.setValue(QDate::fromString(str, "yyyyMMdd")); 1444 | } 1445 | else if (tag.getEVR() == EVR_TM && str.length() == 6) 1446 | { 1447 | value.setValue(QTime::fromString(str, "HHmmss")); 1448 | } 1449 | else if (tag.getEVR() == EVR_DT && str.length() == 14) 1450 | { 1451 | value.setValue(QDateTime::fromString(str, "yyyyMMddHHmmss")); 1452 | } 1453 | else if (tag.getVR().isaString()) 1454 | { 1455 | value.setValue(translateToLatin(str)); 1456 | } 1457 | else 1458 | { 1459 | value = i.value(); 1460 | } 1461 | 1462 | auto cond = putAndInsertVariant(rqDataset, tag, value); 1463 | if (cond.bad()) 1464 | { 1465 | qDebug() << "Failed to set" << tag.getTagName() << "value" << QString::fromLocal8Bit(cond.text()); 1466 | } 1467 | } 1468 | } 1469 | 1470 | reply->deleteLater(); 1471 | return !error; 1472 | } 1473 | 1474 | void PrintSCP::insertTags(DcmDataset *rqDataset, QVariantMap &queryParams, DicomImage *di, QSettings& settings) 1475 | { 1476 | auto tagCount = settings.beginReadArray("tag"); 1477 | QRect prevRect; 1478 | QString ocrText; 1479 | 1480 | for (int i = 0; i < tagCount; ++i) 1481 | { 1482 | settings.setArrayIndex(i); 1483 | auto key = settings.value("key").toString(); 1484 | 1485 | auto rect = settings.value("rect").toRect(); 1486 | if (!rect.isEmpty()) 1487 | { 1488 | if (rect.left() < 0) rect.moveLeft(di->getWidth() + rect.left()); 1489 | if (rect.top() < 0) rect.moveTop(di->getHeight() + rect.top()); 1490 | if (prevRect != rect) 1491 | { 1492 | prevRect = rect; 1493 | #ifdef WITH_TESSERACT 1494 | tess.SetRectangle(rect.left(), rect.top(), rect.width(), rect.height()); 1495 | ocrText = QString::fromUtf8(tess.GetUTF8Text()) 1496 | .remove(reBadSymbols).trimmed(); // remove non printable symbols and trailing whitespace. 1497 | #else 1498 | ocrText = "(built with no OCR support)"; 1499 | #endif 1500 | } 1501 | } 1502 | 1503 | QString str; 1504 | auto pattern = settings.value("pattern").toString(); 1505 | if (!pattern.isEmpty()) 1506 | { 1507 | if (rect.isEmpty()) 1508 | { 1509 | qDebug() << "pattern" << pattern << "ignored since `rect' isn't specified for" << key; 1510 | } 1511 | else if (ocrText.isEmpty()) 1512 | { 1513 | qDebug() << "No text on the image for idx" << i << "key" << key << "rect" << rect; 1514 | } 1515 | else 1516 | { 1517 | QRegExp re(pattern); 1518 | if (re.indexIn(ocrText) < 0) 1519 | { 1520 | qDebug() << ocrText << "does not match" << pattern; 1521 | } 1522 | else 1523 | { 1524 | str = re.cap(1); 1525 | } 1526 | } 1527 | } 1528 | 1529 | // The pattern is absent or mismatched - send the default value 1530 | // 1531 | if (str.isEmpty()) 1532 | { 1533 | str = settings.value("value").toString(); 1534 | } 1535 | 1536 | auto param = settings.value("query-parameter").toString(); 1537 | if (!param.isEmpty()) 1538 | { 1539 | queryParams[param] = str; 1540 | } 1541 | 1542 | if (!key.isEmpty()) 1543 | { 1544 | DcmTag tag; 1545 | if (DcmTag::findTagFromName(key.toUtf8(), tag).bad()) 1546 | { 1547 | qDebug() << "Unknown DCM tag" << key; 1548 | } 1549 | else 1550 | { 1551 | rqDataset->putAndInsertString(tag, str.toUtf8()); 1552 | } 1553 | } 1554 | } 1555 | settings.endArray(); 1556 | } 1557 | --------------------------------------------------------------------------------