Skip to content

Commit

Permalink
Edge cases (#763, #749)
Browse files Browse the repository at this point in the history
  • Loading branch information
neurolabusc committed Nov 26, 2023
1 parent 1ba766d commit 3c7f26b
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 32 deletions.
5 changes: 3 additions & 2 deletions FILENAMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ dcm2niix will attempt to write your image using the naming scheme you specify wi

## Special Characters

[Some characters are not permitted](https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names) in file names. The following characters will be replaced with underscorces (`_`). Note that the forbidden characters vary between operating systems (Linux only forbids the forward slash, MacOS forbids forward slash and colon, while Windows forbids any of the characters listed below). To ensure that files can be easily copied between file systems, [dcm2niix restricts file names to characters allowed by Windows](https://github.com/rordenlab/dcm2niix/issues/237). While technically legal in all filesystems, the semicolon can wreak havoc in [Windows](https://stackoverflow.com/questions/3869594/semi-colons-in-windows-filenames) and [Linux](https://forums.plex.tv/t/linux-hates-semicolons-in-file-names/49098/2).
[Some characters are not permitted](https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names) in file names. The following characters will be replaced with underscorces (`_`). Note that the forbidden characters vary between operating systems (Linux only forbids the forward slash, MacOS forbids forward slash and colon, while Windows forbids any of the characters listed below). To ensure that files can be easily copied between file systems, [dcm2niix restricts file names to characters allowed by Windows](https://github.com/rordenlab/dcm2niix/issues/237). While technically legal in all filesystems, the semicolon can wreak havoc in [Windows](https://stackoverflow.com/questions/3869594/semi-colons-in-windows-filenames) and [Linux](https://forums.plex.tv/t/linux-hates-semicolons-in-file-names/49098/2). Likewise, the [dollar sign can cause issues](https://github.com/rordenlab/dcm2niix/issues/749).

### List of Forbidden Characters
```
Expand All @@ -83,7 +83,8 @@ dcm2niix will attempt to write your image using the naming scheme you specify wi
| (vertical bar or pipe)
? (question mark)
* (asterisk)
; (semicolon)
; (semicolon)
$ (dollar sign)
```

[Control characters](https://en.wikipedia.org/wiki/ASCII#Control_characters) like backspace and tab are also forbidden.
Expand Down
6 changes: 6 additions & 0 deletions console/main_console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,12 @@ int main(int argc, const char *argv[]) {
opts.isAnonymizeBIDS = false;
else
opts.isAnonymizeBIDS = true;
} else if (argv[i][2] == 'i') { //"-bi M2022" provide BIDS subject ID
i++;
snprintf(opts.bidsSubject, kOptsStr-1, "%s", argv[i]);
} else if (argv[i][2] == 'v') { //"-bv 1222" provide BIDS subject visit
i++;
snprintf(opts.bidsSession, kOptsStr-1, "%s", argv[i]);
} else
printf("Error: Unknown command line argument: '%s'\n", argv[i]);
} else if ((argv[i][1] == 'c') && ((i + 1) < argc)) {
Expand Down
8 changes: 6 additions & 2 deletions console/nii_dicom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,7 @@ struct TDICOMdata clear_dicom_data() {
d.CSA.coilNumber = -1;
strcpy(d.CSA.bidsDataType, "");
strcpy(d.CSA.bidsEntitySuffix, "");
strcpy(d.CSA.bidsTask, "");
return d;
} //clear_dicom_data()

Expand Down Expand Up @@ -4327,6 +4328,7 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D
#define kSeriesTime 0x0008 + (0x0031 << 16)
#define kAcquisitionTime 0x0008 + (0x0032 << 16) //TM
//#define kContentTime 0x0008+(0x0033 << 16 ) //TM
#define kAccessionNumber 0x0008 + (0x0050 << 16)
#define kModality 0x0008 + (0x0060 << 16) //CS
#define kManufacturer 0x0008 + (0x0070 << 16)
#define kInstitutionName 0x0008 + (0x0080 << 16)
Expand All @@ -4343,7 +4345,6 @@ struct TDICOMdata readDICOMx(char *fname, struct TDCMprefs *prefs, struct TDTI4D
#define kIconSQ 0x0009 + (0x1110 << 16)
#define kPatientName 0x0010 + (0x0010 << 16)
#define kPatientID 0x0010 + (0x0020 << 16)
#define kAccessionNumber 0x0008 + (0x0050 << 16)
#define kPatientBirthDate 0x0010 + (0x0030 << 16)
#define kPatientSex 0x0010 + (0x0040 << 16)
#define kPatientAge 0x0010 + (0x1010 << 16)
Expand Down Expand Up @@ -4480,6 +4481,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD
#define kScanOptionsSiemens 0x0021 + (0x105C << 16) //CS Siemens ONLY
#define kPATModeText 0x0021 + (0x1009 << 16) //LO, see kImaPATModeText
#define kCSASeriesHeaderInfoXA 0x0021 + (0x1019 << 16)
#define kCSASeriesHeaderInfoXA2 0x0021 + (0x11FE << 16)
#define kTimeAfterStart 0x0021 + (0x1104 << 16) //DS
#define kICE_dims 0x0021 + (0x1106 << 16) //LO [X_4_1_1_1_1_160_1_1_1_1_1_277]
#define kPhaseEncodingDirectionPositiveSiemens 0x0021 + (0x111C << 16) //IS
Expand Down Expand Up @@ -4871,7 +4873,7 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD
}
if ((volumeNumber == 1) && (acquisitionTimePhilips >= 0.0) && (inStackPositionNumber > 0)) {
d.CSA.sliceTiming[inStackPositionNumber - 1] = acquisitionTimePhilips;
printf("%d\t%f\n", inStackPositionNumber, acquisitionTimePhilips);
//printf("%d\t%f\n", inStackPositionNumber, acquisitionTimePhilips);
acquisitionTimePhilips = - 1.0;
}
int ndim = nDimIndxVal;
Expand Down Expand Up @@ -6145,6 +6147,8 @@ const uint32_t kEffectiveTE = 0x0018 + uint32_t(0x9082 << 16); //FD
//printMessage("p%gs%d\n", d.accelFactPE, multiBandFactor);
break;
}
// case kCSASeriesHeaderInfoXA2:
// printf("do something profound\n");
case kCSASeriesHeaderInfoXA:
if (d.manufacturer != kMANUFACTURER_SIEMENS)
break;
Expand Down
3 changes: 2 additions & 1 deletion console/nii_dicom.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extern "C" {
#define kCPUsuf " " //unknown CPU
#endif

#define kDCMdate "v1.0.20231005"
#define kDCMdate "v1.0.20231111"
#define kDCMvers kDCMdate " " kJP2suf kLSsuf kCCsuf kCPUsuf

static const int kMaxEPI3D = 1024; //maximum number of EPI images in Siemens Mosaic
Expand Down Expand Up @@ -232,6 +232,7 @@ static const uint8_t MAX_NUMBER_OF_DIMENSIONS = 8;
bool isPhaseMap;
char bidsDataType[kDICOMStr]; //anat, func, dwi
char bidsEntitySuffix[kDICOMStrLarge]; //anat, func, dwi
char bidsTask[kDICOMStr]; //rest, naming40
};
struct TDICOMdata {
long seriesNum;
Expand Down
127 changes: 101 additions & 26 deletions console/nii_dicom_batch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,14 @@ tse3d: T2*/
json_Float(fp, "\t\"PatientWeight\": %g,\n", d.patientWeight);
//d.patientBirthDate //convert from DICOM YYYYMMDD to JSON
//d.patientAge //4-digit Age String: nnnD, nnnW, nnnM, nnnY;
//issue 763 following BIDS standard, unit for Age is YEARS
int ageLen = strlen(d.patientAge);
if ((ageLen > 1) && (d.patientAge[ageLen-1] == 'Y')) {
char ageStr[kDICOMStr];
strcpy(ageStr, d.patientAge);
ageStr[ageLen -1] = '\0';
fprintf(fp, "\t\"PatientAge\": %d,\n", atoi(ageStr));
}
}
if (d.isQuadruped)
json_Bool(fp, "\t\"Quadruped\": %s,\n", true); // BIDS suggests 0018,9020 but Siemens V-series do not populate this, alternatives are CSA or (0018,0021) CS [SK\MTC\SP]
Expand Down Expand Up @@ -2330,8 +2338,13 @@ int *nii_saveDTI(char pathoutname[], int nConvert, struct TDCMsort dcmSort[], st
allB0 = false;
if (numDti > 1)
allB0 = false;
if (nConvert > 1)
allB0 = false;
if (nConvert > 1) {
for (int i = 0; i < nConvert; i++) { //for each image
float b0 = dcmList[dcmSort[i].indx].CSA.dtiV[0];
if (!isSameFloat(b0, 0.0))
allB0 = false;
}
}
if ((numDti == 1) && (dti4D->S[0].V[0] > 50.0))
allB0 = false;
if (allB0) {
Expand Down Expand Up @@ -3364,6 +3377,16 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts
strcat(outname, dcm.accessionNumber);
if (f == 'H') {
printWarning("hazardous (%%h) bids naming experimental\n");
char bidsSubject[kOptsStr] = "sub-";
if (strlen(opts.bidsSubject) <= 0)
strcat(bidsSubject, "1");
else
strcat(bidsSubject, opts.bidsSubject);
char bidsSession[kOptsStr] = "ses-";
if (strlen(opts.bidsSession) <= 0)
strcat(bidsSession, "1");
else
strcat(bidsSession, opts.bidsSession);
createDummyBidsBoilerplate(pth, (strstr(dcm.CSA.bidsDataType, "func") != NULL));
if (strlen(dcm.CSA.bidsDataType) < 1) {
strcat(outname, "Unknown");
Expand All @@ -3376,13 +3399,22 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts

} else {
isAddNamePostFixes = false;
strcat(outname, "sub-1");
strcat(outname, bidsSubject);
strcat(outname, pathSep);
strcat(outname, bidsSession);
strcat(outname, pathSep);
strcat(outname, dcm.CSA.bidsDataType);
strcat(outname, pathSep);
strcat(outname, "sub-1");
if (strstr(dcm.CSA.bidsDataType, "func") != NULL)
strcat(outname, "_task-rest");
strcat(outname, bidsSubject);
strcat(outname, "_");
strcat(outname, bidsSession);
if (strstr(dcm.CSA.bidsDataType, "func") != NULL) {
strcat(outname, "_task-");
if (strlen(dcm.CSA.bidsTask) > 0)
strcat(outname, dcm.CSA.bidsTask);
else
strcat(outname, "rest");
}
strcat(outname, dcm.CSA.bidsEntitySuffix);
}
}
Expand Down Expand Up @@ -3485,6 +3517,16 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts
else
strcat(outname, "NA");
}
if (f == 'W') {//Weird includes personal data in filename patientWeight
snprintf(newstr, PATH_MAX, "dob%sg%cwt%d", dcm.patientBirthDate, dcm.patientSex, (int)round(dcm.patientWeight));
if (strstr(dcm.institutionName, "Richland"))
strcat(newstr, "R");
strcat(outname, newstr);
}
if ((f == 'Y') && (dcm.rawDataRunNumber >= 0)) {
snprintf(newstr, PATH_MAX, "%d", dcm.rawDataRunNumber); //GE (0019,10A2) else (0020,0100)
strcat(outname, newstr);
}
if (f == 'X')
strcat(outname, dcm.studyID);
if ((f == 'Y') && (dcm.rawDataRunNumber >= 0)) {
Expand Down Expand Up @@ -3629,6 +3671,7 @@ int nii_createFilename(struct TDICOMdata dcm, char *niiFilename, struct TDCMopts
for (size_t pos = 0; pos < strlen(outname); pos++)
if ((outname[pos] == '\\') || (outname[pos] == '/') || (outname[pos] == ' ') || (outname[pos] == '<') || (outname[pos] == '>') || (outname[pos] == ':') || (outname[pos] == ';') || (outname[pos] == '"') // || (outname[pos] == '/') || (outname[pos] == '\\')
//|| (outname[pos] == '^') issue398
|| (outname[pos] == '$') //issue749
|| (outname[pos] == '*') || (outname[pos] == '|') || (outname[pos] == '?'))
outname[pos] = '_';
#else
Expand Down Expand Up @@ -4063,19 +4106,19 @@ int pigz_File(char *fname, struct TDCMopts opts, size_t imgsz) {
strcat(command, opts.pigzname);
if ((opts.gzLevel > 0) && (opts.gzLevel < 12)) {
char newstr[256];
//snprintf(newstr, 256, "\"%s -n -f -%d \"", blockSize, opts.gzLevel);
snprintf(newstr, 256, "\"%s -n -f -%d '", blockSize, opts.gzLevel);
snprintf(newstr, 256, "\"%s -n -f -%d \"", blockSize, opts.gzLevel);
//749 snprintf(newstr, 256, "\"%s -n -f -%d '", blockSize, opts.gzLevel);
strcat(command, newstr);
} else {
char newstr[256];
//snprintf(newstr, 256, "\"%s -n \"", blockSize);
snprintf(newstr, 256, "\"%s -n '", blockSize);
snprintf(newstr, 256, "\"%s -n \"", blockSize);
//749 snprintf(newstr, 256, "\"%s -n '", blockSize);
strcat(command, newstr);
}
strcat(command, fname);
//issue749 use single quote to prevent expansion of $
strcat(command, "'"); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
//strcat(command, "\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
//749 strcat(command, "'"); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
strcat(command, "\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
#if defined(_WIN64) || defined(_WIN32) //using CreateProcess instead of system to run in background (avoids screen flicker)
DWORD exitCode;
PROCESS_INFORMATION ProcessInfo = {0};
Expand Down Expand Up @@ -5262,18 +5305,18 @@ int nii_saveNII(char *niiFilename, struct nifti_1_header hdr, unsigned char *im,
strcat(command, opts.pigzname);
if ((opts.gzLevel > 0) && (opts.gzLevel < 12)) {
char newstr[256];
snprintf(newstr, 256, "\" -n -f -%d > '", opts.gzLevel);
snprintf(newstr, 256, "\" -n -f -%d > \"", opts.gzLevel);
//749 snprintf(newstr, 256, "\" -n -f -%d > '", opts.gzLevel);
strcat(command, newstr);
} else
strcat(command, "\" -n -f > '"); //current versions of pigz (2.3) built on Windows can hang if the filename is included, presumably because it is not finding the path characters ':\'
strcat(command, "\" -n -f > \""); //current versions of pigz (2.3) built on Windows can hang if the filename is included, presumably because it is not finding the path characters ':\'
//749 strcat(command, "\" -n -f > '"); //current versions of pigz (2.3) built on Windows can hang if the filename is included, presumably because it is not finding the path characters ':\'
strcat(command, fname);
//issue749 single not double quotes so $ character does not cause issues
strcat(command, ".gz'"); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
//strcat(command, ".gz\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
//strcat(command, "x.gz\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
//749 strcat(command, ".gz'"); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
strcat(command, ".gz\""); //add quotes in case spaces in filename 'pigz "c:\my dir\img.nii"'
if (opts.isVerbose)
printMessage("Compress: %s\n", command);
printMessage("Compress: %s\n", command);
FILE *pigzPipe;
if ((pigzPipe = popen(command, "w")) == NULL) {
printError("Unable to open pigz pipe\n");
Expand Down Expand Up @@ -6546,7 +6589,7 @@ void setBidsSiemens(struct TDICOMdata *d, int nConvert, int isVerbose, const cha
strcpy(modalityBIDS, "sbref");
//if seriesDesc trace", "fa", "adc" isDerived = true;
isDirLabel = true;
} else if ((strstr(seqDetails, "_asl") != NULL) || (strstr(seqDetails, "_pasl") != NULL) || (strstr(seqDetails, "pcasl") != NULL) || (strstr(seqDetails, "PCASL") != NULL)) { //prog_asl
} else if ((strstr(seqDetails, "fairest")) || (strstr(seqDetails, "_asl") != NULL) || (strstr(seqDetails, "_pasl") != NULL) || (strstr(seqDetails, "pcasl") != NULL) || (strstr(seqDetails, "PCASL") != NULL)) { //prog_asl
strcpy(dataTypeBIDS, "perf");
strcpy(modalityBIDS, "asl");
if (strstr(d->seriesDescription, "_m0") != NULL)
Expand Down Expand Up @@ -6713,6 +6756,28 @@ void setBidsSiemens(struct TDICOMdata *d, int nConvert, int isVerbose, const cha
seqDetails, d->pulseSequenceName, d->sequenceName);
if (isDerived)
strcpy(dataTypeBIDS, "derived");
//bork - ARC data follows
/*
if (strstr(dataTypeBIDS, "dwi")) {
if (strstr(d->protocolName, "12 dirs"))
strcpy(dataTypeBIDS, "discard");
}
if (strstr(dataTypeBIDS, "fmap"))
strcpy(dataTypeBIDS, "discard");
if (strstr(dataTypeBIDS, "perf"))
strcpy(dataTypeBIDS, "discard");
if (strstr(dataTypeBIDS, "func")) {
if (d->TR > 9999) {
if (nConvert < 60)
strcpy(dataTypeBIDS, "discard");
strcpy(d->CSA.bidsTask, "naming40");
} else {
if (!strcasestr(d->protocolName, "REST"))
strcpy(dataTypeBIDS, "discard");
}
if (nConvert < 10)
strcpy(dataTypeBIDS, "discard");
}*/
} // setBidsSiemens()

void setBidsPhilips(struct TDICOMdata *d, int nConvert, int isVerbose) {
Expand Down Expand Up @@ -7050,23 +7115,24 @@ void setBidsGE(struct TDICOMdata *d, int nConvert, int isVerbose, const char *fi
strcpy(dataTypeBIDS, "derived");
} // setBidsGE()

void setBids(struct TDICOMdata *d, const char *filename, int nConvert, int isVerbose) {
bool setBids(struct TDICOMdata *d, const char *filename, int nConvert, int isVerbose) {
if (d->modality == kMODALITY_PT) {
strcpy(d->CSA.bidsDataType, "PET");
strcpy(d->CSA.bidsEntitySuffix, "PET");
return;
return true;
}
if (d->modality == kMODALITY_CT) {
strcpy(d->CSA.bidsDataType, "CT");
strcpy(d->CSA.bidsEntitySuffix, "CT");
return;
return true;
}
if (d->manufacturer == kMANUFACTURER_SIEMENS)
setBidsSiemens(d, nConvert, isVerbose, filename);
if (d->manufacturer == kMANUFACTURER_PHILIPS)
setBidsPhilips(d, nConvert, isVerbose);
if (d->manufacturer == kMANUFACTURER_GE)
setBidsGE(d, nConvert, isVerbose, filename);
return ((!strstr(d->CSA.bidsDataType, "discard")) && (!strstr(d->CSA.bidsDataType, "derived")));
//printf("%s\\%s\n", d->CSA.bidsDataType, d->CSA.bidsEntitySuffix);
}

Expand Down Expand Up @@ -7974,7 +8040,12 @@ int saveDcm2NiiCore(int nConvert, struct TDCMsort dcmSort[], struct TDICOMdata d
if (hdr0.dim[4] > 1) //for 4d datasets, last volume should be acquired before first
checkDateTimeOrder(&dcmList[dcmSort[0].indx], &dcmList[dcmSort[nConvert - 1].indx]);
}
setBids(&dcmList[indx0], nameList->str[dcmSort[0].indx], nConvert, opts.isVerbose);
bool ok = setBids(&dcmList[indx0], nameList->str[dcmSort[0].indx], nConvert, opts.isVerbose);

if (opts.isIgnoreDerivedAnd2D && !ok) {
printMessage("Ignoring derived image(s) of series %ld %s\n", dcmList[indx].seriesNum, nameList->str[indx]);
return EXIT_SUCCESS;
}
int sliceDir = sliceTimingCore(dcmSort, dcmList, &hdr0, opts.isVerbose, nameList->str[dcmSort[0].indx], nConvert, opts);
#ifdef myReportSliceFilenames
if (sliceDir < 0) {
Expand Down Expand Up @@ -9850,9 +9921,14 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe
#endif
#endif
//printMessage("%d %s\n",opts->compressFlag, opts->compressname);
strcpy(opts->indir, "");
strcpy(opts->outdir, "");
strcpy(opts->indir, "");
strcpy(opts->pigzname, "");
strcpy(opts->optsname, "");
strcpy(opts->indirParent, "");
strcpy(opts->imageComments, "");
strcpy(opts->bidsSubject, "");
strcpy(opts->bidsSession, "");
opts->isOnlySingleFile = false; //convert all files in a directory, not just a single file
opts->isOneDirAtATime = false;
opts->isRenameNotConvert = false;
Expand Down Expand Up @@ -9905,8 +9981,7 @@ void setDefaultOpts(struct TDCMopts *opts, const char *argv[]) { //either "setDe
opts->numSeries = 0;
memset(opts->seriesNumber, 0, sizeof(opts->seriesNumber));
strcpy(opts->filename, "%f_%p_%t_%s");

opts->isDumpNotConvert = false;
opts->isDumpNotConvert = false;
} // setDefaultOpts()

#if defined(_WIN64) || defined(_WIN32)
Expand Down
2 changes: 1 addition & 1 deletion console/nii_dicom_batch.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ extern "C" {
bool isDumpNotConvert;
bool isIgnoreTriggerTimes, isTestx0021x105E, isAddNamePostFixes, isSaveNativeEndian, isOneDirAtATime, isRenameNotConvert, isSave3D, isGz, isPipedGz, isFlipY, isCreateBIDS, isSortDTIbyBVal, isAnonymizeBIDS, isOnlyBIDS, isCreateText, isForceOnsetTimes,isIgnoreDerivedAnd2D, isPhilipsFloatNotDisplayScaling, isTiltCorrect, isRGBplanar, isOnlySingleFile, isForceStackDCE, isIgnoreSeriesInstanceUID, isRotate3DAcq, isCrop, isGuessBidsFilename;
int saveFormat, isMaximize16BitRange, isForceStackSameSeries, nameConflictBehavior, isVerbose, isProgress, compressFlag, dirSearchDepth, onlySearchDirForDICOM, gzLevel, diffCyclingModeGE; //support for compressed data 0=none,
char filename[kOptsStr], outdir[kOptsStr], indir[kOptsStr], pigzname[kOptsStr], optsname[kOptsStr], indirParent[kOptsStr], imageComments[24];
char filename[kOptsStr], outdir[kOptsStr], indir[kOptsStr], pigzname[kOptsStr], optsname[kOptsStr], indirParent[kOptsStr], imageComments[24], bidsSubject[kOptsStr], bidsSession[kOptsStr];
double seriesNumber[MAX_NUM_SERIES]; //requires double must store -1 (report but do not convert) as well as seriesUidCrc (uint32)
long numSeries;
#ifdef USING_R
Expand Down

0 comments on commit 3c7f26b

Please sign in to comment.