array('Application Entity',16,0),
'AS' => array('Age String',4,1),
'AT' => array('Attribute Tag',4,1),
'CS' => array('Code String',16,0),
'DA' => array('Date',8,1),
'DS' => array('Decimal String',16,0),
'DT' => array('Date Time',26,0),
'FL' => array('Floating Point Single',4,1),
'FD' => array('Floating Point Double',8,1),
'IS' => array('Integer String',12,0),
'LO' => array('Long String',64,0),
'LT' => array('Long Text',10240,0),
'OB' => array('Other Byte String',0,0),
'OX' => array('Mixed. Other {Byte|Word} String',0,0),
'OW' => array('Other Word String',0,0),
'PN' => array('Person Name',64,0),
'SH' => array('Short String',16,0),
'SL' => array('Signed Long',4,1),
'SQ' => array('Sequence of Items',0,0),
'SS' => array('Signed Short',2,1),
'ST' => array('Short Text',1024,0),
'TM' => array('Time',16,0),
'UI' => array('Unique Identifier UID',64,0),
'UL' => array('Unsigned Long',4,1),
'UN' => array('Unknown',0,0),
'US' => array('Unsigned Short',2,1),
'UT' => array('Unlimited Text',0,0)
);
/**
* This class allows reading and modifying of DICOM files
*/
class File_DICOM extends PEAR
{
/**
* DICOM dictionary.
* @var array
*/
var $dict;
/**
* Flag indicating if the current file is a DICM file or a NEMA file.
* true => DICM, false => NEMA.
*
* @var bool
*/
var $is_dicm;
/**
* Currently open file.
* @var string
*/
var $current_file;
/**
* Initial 0x80 bytes of last read file
* @var string
*/
var $_preamble_buff;
/**
* Array of DICOM elements indexed by group and element index
* @var array
*/
var $_elements;
/**
* Array of DICOM elements indexed by name
* @var array
*/
var $_elements_by_name;
/**
* Transfer Syntax. To be able of reading different types according to standard
* 1.2.840.10008.1.2 Implicit VR, Little Endian
* 1.2.840.10008.1.2.1 Explicit VR, Little Endian
* 1.2.840.10008.1.2.2 Explicit VR, Big Endian
* @var string
*/
var $transfer_syntax;
/**
* Encapsulated Transfer Syntax.
* 1.2.840.10008.1.2.4.50 JPEG baseline
* 1.2.840.10008.1.2.4.51 JPEG extended
* 1.2.840.10008.1.2.4.57 JPEG lossless, non-hierarchical
* 1.2.840.10008.1.2.4.70 JPEG lossless, non-hierarchical, first-order prediction
* @var string
*/
var $_encapsulated_transfer_syntax = array(
"1.2.840.10008.1.2.4.50",
"1.2.840.10008.1.2.4.51",
"1.2.840.10008.1.2.4.57",
"1.2.840.10008.1.2.4.70"
);
/**
* Constructor.
* It creates a File_DICOM object.
*
* @access public
*/
function File_DICOM()
{
/**
* Initialize dictionary.
*/
global $dictionary;
$this->dict = $dictionary->dict;
}
/**
* Utility to know if a file is DICOM or not.
* It is not a bullet-proof test.
*
* @access public
* @param string $infile The DICOM file to parse
* @return boolean true if file is DICOM, or false is file does not satisfy the preamble test.
* Notice, that a file returning false could still be DICOM.
*/
function isDicom($infile) {
return $this->parse($infile, true);
}
/**
* Parse a DICOM file
* Parse a DICOM file and get all of its header members
*
* @access public
* @param string $infile The DICOM file to parse
* @param boolean $only_check_type Default to false. It is true when called by method isDicom
* @return mixed true on success or DICOM file, PEAR_Error on failure, or false when is not a DICOM file
*/
function parse($infile, $only_check_type = false)
{
$this->current_file = $infile;
// Fill the transfer syntax by default to Explicit VR Little Endian
$this->transfer_syntax = EXPLICIT_VR_LITTLE_ENDIAN;
$fh = @fopen($infile, "rb");
if (!$fh) {
return $this->raiseError("Could not open file $infile for reading");
}
$stat = fstat($fh);
$this->_file_length = $stat[7];
// Test for NEMA or DICOM file.
// If DICM, store initial preamble and leave file ptr at 0x84.
$this->_preamble_buff = fread($fh, 0x80);
$buff = fread($fh, 4);
$this->is_dicm = ($buff == 'DICM');
if ($only_check_type) {
fclose($fh);
return $this->is_dicm;
}
if (!$this->is_dicm) {
fseek($fh, 0);
}
$last_group = 0x0000;
// Fill in hash with header members from given file.
while (ftell($fh) < $this->_file_length)
{
$element =& new File_DICOM_Element($fh, $this->dict,$this->transfer_syntax,$last_group);
$this->_elements[$element->group][$element->element][] =& $element;
$this->_elements_by_name[$element->name][] =& $element;
$last_group = $element->group;
// Fill the proper transfer syntax
if ($element->group == 0x0002 && $element->element == 0x0010) {
$this->transfer_syntax = $element->value;
}
}
fclose($fh);
return true;
}
/**
* Write current contents to a DICOM file.
*
* @access public
* @param string $outfile The name of the file to write. If not given
* it assumes the name of the file parsed.
* If no file was parsed and no name is given
* returns a PEAR_Error
* @return mixed true on success, PEAR_Error on failure
*/
function write($outfile = '')
{
if ($outfile == '') {
if (isset($this->current_file)) {
$outfile = $this->current_file;
} else {
return $this->raiseError("File name not given (and no file currently open)");
}
}
$fh = @fopen($outfile, "wb");
if (!$fh) {
return $this->raiseError("Could not open file $outfile for writing");
}
// Writing file from scratch will always fail for now
if (!isset($this->_preamble_buff)) {
return $this->raiseError("Cannot write DICOM file from scratch");
}
// Don't store initial preamble and DICM word. Working with NEMA.
//fwrite($fh, $this->_preamble_buff);
//fwrite($fh, 'DICM');
/*$buff = fread($fh, 4);
$this->is_dicm = ($buff == 'DICM');
if (!$this->is_dicm) {
fseek($fh, 0);
}*/
// There are not that much groups/elements. Using foreach()
foreach (array_keys($this->_elements) as $group) {
foreach (array_keys($this->_elements[$group]) as $element) {
foreach (array_keys($this->_elements[$group][$element]) as $z) {
fwrite($fh, pack('v', $group));
fwrite($fh, pack('v', $element));
$code = $this->_elements[$group][$element][$z]->code;
// Preserve the VR type from the file parsed
if (($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) or ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_16_BITS)) {
fwrite($fh, pack('CC', $code{0}, $code{1}));
// This is an OB, OW, SQ, UN or UT: 32 bit VL field.
if ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_EXPLICIT_32_BITS) {
fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length));
} else { // not using fixed length from VR!!!
fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->length));
}
} elseif ($this->_elements[$group][$element][$z]->vr_type == FILE_DICOM_VR_TYPE_IMPLICIT) {
fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->length));
}
switch ($code) {
// Decode unsigned longs and shorts.
case 'UL':
fwrite($fh, pack('V', $this->_elements[$group][$element][$z]->value));
break;
case 'US':
fwrite($fh, pack('v', $this->_elements[$group][$element][$z]->value));
break;
// Floats: Single and double precision.
case 'FL':
fwrite($fh, pack('f', $this->_elements[$group][$element][$z]->value));
break;
case 'FD':
fwrite($fh, pack('d', $this->_elements[$group][$element][$z]->value));
break;
// Binary data. Only save position. Is this right?
case 'OW':
case 'OB':
case 'OX':
// Binary data. Value only contains position on the parsed file.
// Will fail when file name for writing is the same as for parsing.
$fh2 = @fopen($this->current_file, "rb");
if (!$fh2) {
return $this->raiseError("Could not open file {$this->current_file} for reading");
}
fseek($fh2, $this->_elements[$group][$element][$z]->value);
fwrite($fh, fread($fh2, $this->_elements[$group][$element][$z]->length));
fclose($fh2);
break;
default:
fwrite($fh, $this->_elements[$group][$element][$z]->value, $this->_elements[$group][$element][$z]->length);
break;
}
}
}
}
fclose($fh);
return true;
}
/**
* Gets the value for a DICOM element
* Gets the value for a DICOM element of a given group from the
* parsed DICOM file.
*
* @access public
* @param mixed $gp_or_name The group the DICOM element belongs to
* (integer), or it's name (string)
* @param integer $el The identifier for the DICOM element
* (unique inside a group)
* @param integer $z The 3rd dimension for the element (created for sequences)
* By default is set to 0 for backwards compatibility
* (usually for single elements).
* @return mixed The value for the DICOM element on success, PEAR_Error on failure
*/
function getValue($gp_or_name, $el = null, $z = 0)
{
if (isset($el)) // retreive by group and element index
{
if (isset($this->_elements[$gp_or_name][$el][$z])) {
$value = $this->_elements[$gp_or_name][$el][$z]->getValue();
if (empty($value)) {
return null;
}
return $this->_elements[$gp_or_name][$el][$z]->getValue();
}
else {
$this->raiseError("Element ($gp_or_name,$el) not found");
return null;
}
}
else // retrieve by name
{
if (isset($this->_elements_by_name[$gp_or_name][$z])) {
$value = $this->_elements_by_name[$gp_or_name][$z]->getValue();
if (empty($value)) {
return null;
}
return $this->_elements_by_name[$gp_or_name][$z]->getValue();
}
else {
$this->raiseError("Element $gp_or_name not found");
return null;
}
}
}
/**
* Sets the value for a DICOM element
* Only works with strings now.
*
* @access public
* @param integer $gp The group the DICOM element belongs to
* @param integer $el The identifier for the DICOM element (unique inside a group)
* @param integer $z The index of the element. By default 0 (for single elements)
*/
function setValue($gp, $el, $value, $z = 0)
{
$this->_elements[$gp][$el][$z]->value = $value;
$this->_elements[$gp][$el][$z]->length = strlen($value);
}
/**
* Dumps the contents of the image inside the DICOM file
* (element 0x0010 from group 0x7FE0) to a PGM (Portable Gray Map) file.
* Use with Caution!!. For a 8.5MB DICOM file on a P4 it takes 28
* seconds to dump it's image.
*
* @access public
* @param string $filename The file where to save the image
* @return mixed true on success, PEAR_Error on failure.
*/
function dumpImage($filename)
{
$length = $this->_elements[0x7FE0][0x0010]->length;
$rows = $this->getValue(0x0028,0x0010);
$cols = $this->getValue(0x0028,0x0011);
$fh = @fopen($filename, "wb");
if (!$fh) {
return $this->raiseError("Could not open file $filename for writing");
}
// magick word
fwrite($fh, "P5\n");
// comment
fwrite($fh, "# file generated by PEAR::File_DICOM on ".strftime("%Y-%m-%d", time())." \n");
fwrite($fh, "# do not use for diagnosing purposes\n");
fwrite($fh, "$cols $rows\n");
// always 255 grays
fwrite($fh, "255\n");
$pos = $this->getValue(0x7FE0,0x0010);
$fh_in = @fopen($this->current_file, "rb");
if (!$fh_in) {
return $this->raiseError("Could not open file {$this->current_file} for reading");
}
fseek($fh_in, $pos);
$block_size = 4096;
$blocks = ceil($length / $block_size);
for ($i = 0; $i < $blocks; $i++) {
if ($i == $blocks - 1) { // last block
$chunk_length = ($length % $block_size) ? ($length % $block_size) : $block_size;
} else {
$chunk_length = $block_size;
}
$chunk = fread($fh_in, $chunk_length);
$pgm = '';
$half_chunk_length = $chunk_length/2;
$rr = unpack("v$half_chunk_length", $chunk);
for ($j = 1; $j <= $half_chunk_length; $j++) {
$pgm .= pack('C', $rr[$j] >> 4);
}
fwrite($fh, $pgm);
}
fclose($fh_in);
fclose($fh);
return true;
}
/**
* Creates an array of GD image object.
*
* @access public
* @param string $filename The file where to save the image
* @return mixed image object array on success,
* PEAR_Error on failure or NULL if GD is not configured.
*/
function imagecreatefromDICOM($filename, $window = null, $level = null)
{
if (!function_exists('imagecreatetruecolor')) {
return NULL;
}
// Let's read some values from DICOM file
$length = $this->_elements[0x7FE0][0x0010][0]->length;
$rows = $this->getValue(0x0028, 0x0010);
$cols = $this->getValue(0x0028, 0x0011);
$samples_per_pixel = $this->getValue(0x0028, 0x0002);
$bits = $this->getValue(0x0028, 0x0100);
$high_bit = $this->getValue(0x0028, 0x0102);
$dose_scaling = (empty($this->_elements[0x3004][0x000E][0]->value))? 1 : $this->getValue(0x3004, 0x000E);
$window_center = (empty($this->_elements[0x0028][0x1050][0]->value))? 0 : $this->getValue(0x0028, 0x1050);
$window_width = (empty($this->_elements[0x0028][0x1051][0]->value))? 0 : $this->getValue(0x0028, 0x1051);
$rescale_intercept = (empty($this->_elements[0x0028][0x1052][0]->value))? 0 : $this->getValue(0x0028, 0x1052);
$rescale_slope = (empty($this->_elements[0x0028][0x1053][0]->value))? 1 : $this->getValue(0x0028, 0x1053);
$number_of_frames = (empty($this->_elements[0x0028][0x0008][0]->value))? 1 : $this->getValue(0x0028, 0x0008);
$pixel_representation = $this->getValue(0x0028, 0x0103);
$photometric_interpretation = $this->getValue(0x0028, 0x0004);
$starting_position = $this->getValue(0x7FE0, 0x0010);
$transfer_syntax_uid = $this->getValue(0x0002, 0x0010);
//$this->_elements = null;
if (in_array($transfer_syntax_uid, $this->_encapsulated_transfer_syntax)) {
// Sorry, encapsulated data is more complex to display... by now.
return NULL;
}
if ($rows * $cols >= 500000) {
// Sorry, file is too big, conver it in a different way
return NULL;
}
// Window Center and Width can have multiple values. By now, just reading the first one. It assumes the delimiter
// is the "\"
if (!(strpos($window_center,"\\") === false)) {
$temp = explode("\\",$window_center);
$window_center = intval($temp[0]);
}
if (!(strpos($window_width,"\\") === false)) {
$temp = explode("\\",$window_width);
$window_width = intval($temp[0]);
}
// Opening the file
$fh = fopen ($filename, 'rb');
if (!$fh) {
return $this->raiseError("Could not open file $filename for writing");
}
fseek($fh, $starting_position);
// $data holds the pixel values
$maximum = array();
$minimum = array();
$images = array();
$data = array();
$max = array();
$min = array();
$current_position = $starting_position;
$current_image = 0;
$bytes_to_read = $bits/8;
$size_image = $cols * $rows * $bytes_to_read * $samples_per_pixel;
$length = $size_image * $number_of_frames;
while (ftell($fh) < $starting_position + $length) {
if ($current_position == $starting_position + $current_image*$size_image) {
$x = 0;
$y = 0;
for ($i = 0; $i < $samples_per_pixel; $i++) {
$max[$current_image][$i] = -20000; // Small enough so it will be properly calculated
$min[$current_image][$i] = 20000; // Large enough so it wil be properly calculated
}
$current_image++;
}
for ($i = 0; $i < $samples_per_pixel; $i++) {
$chunk = fread($fh, $bytes_to_read);
$current_position += $bytes_to_read;
$pixel_value = base_convert(bin2hex(strrev($chunk)), 16, 10);
// Now we have the pixel value
// Checking if 2's complement
$pixel_value = ($pixel_representation)? $this->complement2($pixel_value, $high_bit) : $pixel_value;
// Getting the right value according to slope and intercept
$pixel_value = $pixel_value*$rescale_slope + $rescale_intercept;
// Multiplying for dose_grid_scaling
$pixel_value = $pixel_value*$dose_scaling;
// Assigning the value
$data[$current_image - 1][$x][$y][$i] = $pixel_value;
// Getting the max
if ($pixel_value > $max[$current_image - 1][$i]) {
$max[$current_image - 1][$i] = $pixel_value; // max
}
// Getting the min
if ($pixel_value < $min[$current_image - 1][$i]) {
$min[$current_image - 1][$i] = $pixel_value; // min
}
}
$y++;
if ($y == $cols) { // Next row
$x++;
$y=0;
}
}
fclose ( $fh );
for ($index = 0; $index < $current_image; $index++) {
for ($i = 0; $i < $samples_per_pixel; $i++) {
// Real max and min according to window center & width (if set)
$maximum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center + $window_width/2) : $max[$index][$i];
$minimum[$i] = ($window_center != 0 && $window_width != 0)? round($window_center - $window_width/2) : $min[$index][$i];
// Check if window and level are sent
$maximum[$i] = (!empty($window) && !empty($level))? round($level + $window/2) : $maximum[$i];
$minimum[$i] = (!empty($window) && !empty($level))? round($level - $window/2) : $minimum[$i];
//echo $index . " " . $maximum . " " . $minimum . "
\n";
if ($maximum[$i] == $minimum[$i]) { // Something wrong. Avoid having a zero division
return NULL;
}
}
$img = imagecreatetruecolor($cols, $rows);
for($x =0; $x < $rows;$x++) {
for ($y = 0; $y < $cols; $y++) {
$pixel_values = array();
for ($i = 0; $i < $samples_per_pixel; $i++) {
$pixel_value = $data[$index][$x][$y][$i];
// truncating pixel values over max and below min
$pixel_value = ($pixel_value > $maximum[$i])? $maximum[$i] : $pixel_value;
$pixel_value = ($pixel_value < $minimum[$i])? $minimum[$i] : $pixel_value;
// Converting to gray value
$pixel_value = ($pixel_value - $minimum[$i])/($maximum[$i] - $minimum[$i])*255;
// For MONOCHROME1 we have to invert the pixel values.
if ($photometric_interpretation == "MONOCHROME1") {
$pixel_value = 255 - $pixel_value;
}
$pixel_values[$i] = $pixel_value;
}
if ($samples_per_pixel == 1) {
$color = imagecolorallocate($img, $pixel_values[0], $pixel_values[0], $pixel_values[0]);
}
else {
$color = imagecolorallocate($img, $pixel_values[0], $pixel_values[1], $pixel_values[2]);
}
imagesetpixel($img, $y, $x, $color);
}
}
$images[] = $img;
}
return $images;
}
/**
* Return an integer that was saved as complement 2 .
*
* @access public
* @param integer $number The integer number to convert
* @param integer $high_bit The high bit for the value. By default 15 (assumes 2 bytes)
* @return integer the number after complement's 2 applied
*/
function complement2($number, $high_bit = 15)
{
$sign = $number >> $high_bit;
if ($sign) { // Negative
$number = pow(2, $high_bit + 1) - $number;
$number = -1 * $number;
}
return $number;
}
/**
* Returns an html string with the DICOM file parsed.
*
* @access public
* @param object $dicom_element A Dicom element. It could be a sequence too.
* @param string $prefix To have a nice display at the beginning of each line
* @return string An html string
*/
function htmlParse($dicom_element="", $prefix="Element")
{
if ($dicom_element == "") {
$dicom_element = $this;
}
$data = "";
$count = 1;
if (count($dicom_element->_elements) > 0) {
foreach (array_keys($dicom_element->_elements) as $group) { // Loading groups
foreach (array_keys($dicom_element->_elements[$group]) as $element) { // Loading elements
foreach ($dicom_element->_elements[$group][$element] as $thisElement) { // Loading items in element (Sequences or single element)
$value = ($thisElement->name == "UNKNOWN")? "Propriatory": $thisElement->value;
$data .= " $prefix.$count - ".$thisElement->name . " [" . $this->strHex($group) . "][" . $this->strHex($element) . "] (Length: ". $thisElement->length . ") = " . $value . "
";
$data .= $this->htmlParse($thisElement,"$prefix.$count");
$count++;
}
}
}
}
return $data;
}
/**
* Returns an string as 0x0000. This might be done also using sprintf or alike but not so familiar with that.
*
* @access public
* @param integer $value An integer value. Either the group or element number.
* @return string $string An string with the format 0x0000.
*/
function strHex($value)
{
$string = "0x" . str_pad(strtoupper(dechex($value)), 4, "0", STR_PAD_LEFT);
return $string;
}
}
?>