gimp/plug-ins/common/dicom.c

863 lines
26 KiB
C

/* The GIMP -- an image manipulation program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
* PNM reading and writing code Copyright (C) 1996 Erik Nygren
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
/*
* The dicom reading and writing code was written from scratch
* by Dov Grobgeld. (dov@imagic.weizman.ac.il).
*/
#include "config.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>
#include "libgimp/stdplugins-intl.h"
/* A lot of Dicom images are wrongly encoded. By guessing the endian
* we can get around this problem.
*/
#define GUESS_ENDIAN 1
/* Declare local data types */
typedef struct _DicomInfo
{
gint width, height; /* The size of the image */
gint maxval; /* For 16 and 24 bit image files, the max
value which we need to normalize to */
gint samples_per_pixel; /* Number of image planes (0 for pbm) */
gint bpp;
} DicomInfo;
/* Local function prototypes */
static void query (void);
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals);
static gint32 load_image (const gchar *filename);
static gboolean save_image (const gchar *filename,
gint32 image_ID,
gint32 drawable_ID);
static void dicom_loader (guint8 *pix_buf,
DicomInfo *info,
GimpPixelRgn *pixel_rgn);
static void guess_and_set_endian2 (guint16 *buf16,
gint length);
static void toggle_endian2 (guint16 *buf16,
gint length);
static void add_tag_pointer (GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
guint8 *data,
gint length);
static void add_tag_string (GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
gchar *s);
static void add_tag_int (GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
gint value);
static gboolean write_group_to_file (FILE *DICOM,
gint group,
GByteArray *group_stream);
GimpPlugInInfo PLUG_IN_INFO =
{
NULL, /* init_proc */
NULL, /* quit_proc */
query, /* query_proc */
run, /* run_proc */
};
MAIN ()
static void
query (void)
{
static GimpParamDef load_args[] =
{
{ GIMP_PDB_INT32, "run_mode", "Interactive, non-interactive" },
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
{ GIMP_PDB_STRING, "raw_filename", "The name of the file to load" }
};
static GimpParamDef load_return_vals[] =
{
{ GIMP_PDB_IMAGE, "image", "Output image" }
};
static GimpParamDef save_args[] =
{
{ GIMP_PDB_INT32, "run_mode", "Interactive, non-interactive" },
{ GIMP_PDB_IMAGE, "image", "Input image" },
{ GIMP_PDB_DRAWABLE, "drawable", "Drawable to save" },
{ GIMP_PDB_STRING, "filename", "The name of the file to save" },
{ GIMP_PDB_STRING, "raw_filename", "The name of the file to save" },
};
gimp_install_procedure ("file_dicom_save",
"Save file in the DICOM file format",
"Save an image in the medical standard DICOM image "
"formats. The standard is defined at "
"http://medical.nema.org/. The file format is "
"defined in section 10 of the standard. The files "
"are saved uncompressed and the compulsory DICOM "
"tags are filled with default dummy values.",
"Dov Grobgeld",
"Dov Grobgeld <dov@imagic.weizmann.ac.il>",
"2003",
"<Save>/DICOM",
"RGB, GRAY",
GIMP_PLUGIN,
G_N_ELEMENTS (save_args), 0,
save_args, NULL);
gimp_install_procedure ("file_dicom_load",
"loads files of the dicom file format",
"Load a file in the DICOM standard format."
"The standard is defined at "
"http://medical.nema.org/. The plug-in currently "
"only supports reading images with uncompressed "
"pixel sections.",
"Dov Grobgeld",
"Dov Grobgeld <dov@imagic.weizmann.ac.il>",
"2003",
"<Load>/DICOM",
NULL,
GIMP_PLUGIN,
G_N_ELEMENTS (load_args),
G_N_ELEMENTS (load_return_vals),
load_args, load_return_vals);
gimp_register_magic_load_handler ("file_dicom_load",
"dcm,DCM,dicom,DICOM",
"",
"128,string,DICM"
);
gimp_register_save_handler ("file_dicom_save",
"dicom,dcm,DCM,DICOM",
"");
}
static void
run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals)
{
static GimpParam values[2];
GimpRunMode run_mode;
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
gint32 image_ID;
gint32 drawable_ID;
GimpExportReturn export = GIMP_EXPORT_CANCEL;
INIT_I18N ();
run_mode = param[0].data.d_int32;
*nreturn_vals = 1;
*return_vals = values;
values[0].type = GIMP_PDB_STATUS;
values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR;
if (strcmp (name, "file_dicom_load") == 0)
{
image_ID = load_image (param[1].data.d_string);
if (image_ID != -1)
{
*nreturn_vals = 2;
values[1].type = GIMP_PDB_IMAGE;
values[1].data.d_image = image_ID;
}
else
{
status = GIMP_PDB_EXECUTION_ERROR;
}
}
else if (strcmp (name, "file_dicom_save") == 0)
{
image_ID = param[1].data.d_int32;
drawable_ID = param[2].data.d_int32;
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
case GIMP_RUN_WITH_LAST_VALS:
gimp_ui_init ("dicom", FALSE);
export = gimp_export_image (&image_ID, &drawable_ID, "DICOM",
GIMP_EXPORT_CAN_HANDLE_RGB |
GIMP_EXPORT_CAN_HANDLE_GRAY);
if (export == GIMP_EXPORT_CANCEL)
{
values[0].data.d_status = GIMP_PDB_CANCEL;
return;
}
break;
default:
break;
}
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
break;
case GIMP_RUN_NONINTERACTIVE:
/* Make sure all the arguments are there! */
if (nparams != 5)
status = GIMP_PDB_CALLING_ERROR;
break;
case GIMP_RUN_WITH_LAST_VALS:
break;
default:
break;
}
if (status == GIMP_PDB_SUCCESS)
{
if (! save_image (param[3].data.d_string, image_ID, drawable_ID))
status = GIMP_PDB_EXECUTION_ERROR;
}
if (export == GIMP_EXPORT_EXPORT)
gimp_image_delete (image_ID);
}
else
{
status = GIMP_PDB_CALLING_ERROR;
}
values[0].data.d_status = status;
}
static gint32
load_image (const gchar *filename)
{
GimpPixelRgn pixel_rgn;
gint32 volatile image_ID = -1;
gint32 layer_ID;
GimpDrawable *drawable;
FILE *DICOM;
gchar *temp;
gchar buf[500]; /* buffer for random things like scanning */
DicomInfo *dicominfo;
gint width = 0;
gint height = 0;
gint samples_per_pixel = 0;
gint bpp = 0;
guint8 *pix_buf = NULL;
gboolean toggle_endian = FALSE;
temp = g_strdup_printf (_("Opening '%s'..."), filename);
gimp_progress_init (temp);
g_free (temp);
/* open the file */
DICOM = fopen (filename, "rb");
if (!DICOM)
{
g_message (_("Could not open '%s' for reading: %s"),
filename, g_strerror (errno));
return -1;
}
/* allocate the necessary structures */
dicominfo = g_new0 (DicomInfo, 1);
/* Parse the file */
fread (buf, 1, 128, DICOM); /* skip past buffer */
/* Check for unsupported formats */
if (g_ascii_strncasecmp (buf, "PAPYRUS", 7) == 0)
{
g_message ("'%s' is a PAPYRUS DICOM file.\n"
"This plug-in does not support this type yet.", filename);
return -1;
}
fread (buf, 1, 4, DICOM); /* This should be dicom */
if (g_ascii_strncasecmp (buf,"DICM",4) != 0)
{
g_message (_("'%s' is not a DICOM file."), filename);
return -1;
}
while (TRUE)
{
guint16 group_word;
guint16 element_word;
gchar value_rep[3];
guint32 element_length;
guint32 ctx_ul;
guint16 ctx_us;
guint8 *value;
if (fread (&group_word, 1, 2, DICOM) == 0)
break;
group_word = g_ntohs (GUINT16_SWAP_LE_BE (group_word));
fread (&element_word, 1, 2, DICOM);
element_word = g_ntohs (GUINT16_SWAP_LE_BE (element_word));
fread (value_rep, 1, 2, DICOM);
value_rep[2] = 0;
/* Implicit encoding */
if (value_rep[0] < 'A' ||
value_rep[0] > 'Z' ||
value_rep[1] < 'A' ||
value_rep[1] > 'Z')
{
/* Look up type from the dictionary. At the time we dont
support this option... */
gchar element_length_chars[4];
/* Store the bytes that were read */
element_length_chars[0] = value_rep[0];
element_length_chars[1] = value_rep[1];
/* Unknown value rep. It is not used right now anyhow */
strcpy (value_rep, "??");
/* For implicit value_values the length is always four bytes,
so we need to read another two. */
fread (&element_length_chars[2], 1, 2, DICOM);
/* Now cast to integer and insert into element_length */
element_length =
g_ntohl (GUINT32_SWAP_LE_BE (*((gint *) element_length_chars)));
}
/* Binary value reps are OB, OW, SQ or UN */
else if (strncmp (value_rep, "OB", 2) == 0
|| strncmp (value_rep, "OW", 2) == 0
|| strncmp (value_rep, "SQ", 2) == 0
|| strncmp (value_rep, "UN", 2) == 0)
{
fread (&element_length, 1, 2, DICOM); /* skip two bytes */
fread (&element_length, 1, 4, DICOM);
element_length = g_ntohl (GUINT32_SWAP_LE_BE (element_length));
}
else
{
guint16 el16;
fread (&el16, 1, 2, DICOM);
element_length = g_ntohs (GUINT16_SWAP_LE_BE (el16));
}
/* Read contents. Allocate a bit more to make room for casts to int
below. */
value = g_new0 (guint8, element_length + 4);
fread (value, 1, element_length, DICOM);
/* Some special casts that are used below */
ctx_ul = *(guint32 *) value;
ctx_us = *(guint16 *) value;
/* Recognize some critical tags */
if (group_word == 0x0028)
{
switch (element_word)
{
case 0x0002: /* samples per pixel */
samples_per_pixel = ctx_us;
break;
case 0x0010: /* rows */
height = ctx_us;
break;
case 0x0011: /* columns */
width = ctx_us;
break;
case 0x0100: /* bits_allocated */
bpp = ctx_us;
break;
case 0x0103: /* pixel representation */
toggle_endian = ctx_us;
break;
}
}
/* Pixel data */
if (group_word == 0x7fe0 && element_word == 0x0010)
{
pix_buf = value;
}
else
{
g_free (value);
}
}
dicominfo->width = width;
dicominfo->height = height;
dicominfo->bpp = bpp;
dicominfo->samples_per_pixel = samples_per_pixel;
dicominfo->maxval = -1; /* External normalization factor - not used yet */
/* Create a new image of the proper size and associate the filename with it.
*/
image_ID = gimp_image_new (dicominfo->width, dicominfo->height,
(dicominfo->samples_per_pixel >= 3 ?
GIMP_RGB : GIMP_GRAY));
gimp_image_set_filename (image_ID, filename);
layer_ID = gimp_layer_new (image_ID, _("Background"),
dicominfo->width, dicominfo->height,
(dicominfo->samples_per_pixel >= 3 ?
GIMP_RGB_IMAGE : GIMP_GRAY_IMAGE),
100, GIMP_NORMAL_MODE);
gimp_image_add_layer (image_ID, layer_ID, 0);
drawable = gimp_drawable_get (layer_ID);
gimp_pixel_rgn_init (&pixel_rgn, drawable,
0, 0, drawable->width, drawable->height, TRUE, FALSE);
#if GUESS_ENDIAN
if (bpp == 16)
guess_and_set_endian2 ((guint16 *) pix_buf, width * height);
#endif
dicom_loader (pix_buf, dicominfo, &pixel_rgn);
/* free the structures */
g_free (pix_buf);
g_free (dicominfo);
/* close the file */
fclose (DICOM);
/* Tell the GIMP to display the image. */
gimp_drawable_flush (drawable);
return image_ID;
}
static void
dicom_loader (guint8 *pix_buffer,
DicomInfo *info,
GimpPixelRgn *pixel_rgn)
{
guchar *data, *d;
gint row_idx, i;
gint start, end, scanlines;
gint width = info->width;
gint height = info->height;
gint samples_per_pixel = info->samples_per_pixel;
guint16 *buf16 = (guint16 *) pix_buffer;
gint pix_idx;
guint16 max = 0;
if (info->bpp == 16)
{
/* Reorder the buffer and look for max */
max = 0;
for (pix_idx=0; pix_idx < width * height * samples_per_pixel; pix_idx++)
{
guint pix_gl = g_ntohs (GUINT16_SWAP_LE_BE (buf16[pix_idx]));
if (pix_gl > max)
max = pix_gl;
buf16[pix_idx] = pix_gl;
}
}
data = g_malloc (gimp_tile_height () * width * samples_per_pixel);
for (row_idx = 0; row_idx < height; )
{
start = row_idx;
end = row_idx + gimp_tile_height ();
end = MIN (end, height);
scanlines = end - start;
d = data;
for (i = 0; i < scanlines; i++)
{
if (info->bpp == 16)
{
gint col_idx;
guint16 *row_start;
row_start = buf16 + (row_idx + i) * width * samples_per_pixel;
for (col_idx = 0; col_idx < width * samples_per_pixel; col_idx++)
d[col_idx] =
(guint8) (255.0 * (gdouble)(row_start[col_idx]) / max);
}
else if (info->bpp == 8)
{
gint col_idx;
guint8 *row_start;
row_start = pix_buffer + (row_idx + i) * width * samples_per_pixel;
for (col_idx = 0; col_idx < width * samples_per_pixel; col_idx++)
d[col_idx] = row_start[col_idx];
}
d += width * samples_per_pixel;
}
gimp_progress_update ((gdouble) row_idx / (gdouble) height);
gimp_pixel_rgn_set_rect (pixel_rgn, data, 0, row_idx, width, scanlines);
row_idx += scanlines;
}
g_free (data);
}
/* Guess and set endian. Guesses the endian of a buffer by
* checking the maximum value of the first and the last byte
* in the words of the buffer. It assumes that the least
* significant byte has a larger maximum than the most
* significant byte.
*/
static void
guess_and_set_endian2 (guint16 *buf16,
int length)
{
guint16 *p = buf16;
gint max_first = -1;
gint max_second = -1;
while (p<buf16+length)
{
if (*(guint8*)p > max_first)
max_first = *(guint8*)p;
if (((guint8*)p)[1] > max_second)
max_second = ((guint8*)p)[1];
p++;
}
if ( ((max_second > max_first) && (G_BYTE_ORDER == G_LITTLE_ENDIAN))
|| ((max_second < max_first) && (G_BYTE_ORDER == G_BIG_ENDIAN)))
toggle_endian2 (buf16, length);
}
/* toggle_endian2 toggles the endian for a 16 bit entity. */
static void
toggle_endian2 (guint16 *buf16,
gint length)
{
guint16 *p = buf16;
while (p < buf16 + length)
{
*p = ((*p & 0xff) << 8) | (*p >> 8);
p++;
}
}
/* save_image() saves an image in the dicom format. The DICOM format
* requires a lot of tags to be set. Some of them have real uses, others
* must just be filled with dummy values.
*/
static gboolean
save_image (const gchar *filename,
gint32 image_ID,
gint32 drawable_ID)
{
FILE *DICOM;
GimpImageType drawable_type;
GimpDrawable *drawable;
GimpPixelRgn pixel_rgn;
GByteArray *group_stream;
gint group;
GDate *date;
gchar today_string[16];
gchar *photometric_interp;
gint samples_per_pixel;
gboolean retval = TRUE;
drawable_type = gimp_drawable_type (drawable_ID);
drawable = gimp_drawable_get (drawable_ID);
/* Make sure we're not saving an image with an alpha channel */
if (gimp_drawable_has_alpha (drawable_ID))
{
g_message (_("Cannot save images with alpha channel."));
return FALSE;
}
switch (drawable_type)
{
case GIMP_GRAY_IMAGE:
samples_per_pixel = 1;
photometric_interp = "MONOCHROME2";
break;
case GIMP_RGB_IMAGE:
samples_per_pixel = 3;
photometric_interp = "RGB";
break;
default:
g_message (_("Cannot operate on unknown image types."));
return FALSE;
}
date = g_date_new ();
g_date_set_time (date, time (NULL));
g_snprintf (today_string, sizeof (today_string),
"%04d%02d%02d", date->year, date->month, date->day);
g_date_free (date);
/* Open the output file. */
DICOM = fopen (filename, "wb");
if (!DICOM)
{
g_message (_("Could not open '%s' for writing: %s"),
filename, g_strerror (errno));
gimp_drawable_detach (drawable);
return FALSE;
}
/* Print dicom header */
{
guint8 val = 0;
gint i;
for (i = 0; i < 0x80; i++)
fwrite (&val, 1, 1, DICOM);
}
fprintf (DICOM, "DICM");
group_stream = g_byte_array_new ();
/* Meta element group */
group = 0x0002;
/* 0002,0001 - File Meta Information Version */
add_tag_pointer (group_stream, group, 0x0001, "OB", "\0\1", 2);
/* 0002,0010 - Transfer syntax uid */
add_tag_string (group_stream, group, 0x0010, "UI", "1.2.840.10008.1.2.1");
/* 0002,0013 - Implementation version name */
add_tag_string (group_stream, group, 0x0013, "SH", "Gimp Dicom Plugin 1.0");
write_group_to_file (DICOM, group, group_stream);
/* Identifying group */
group = 0x0008;
/* Study date */
add_tag_string (group_stream, group, 0x0020, "DA", today_string);
/* Series date */
add_tag_string (group_stream, group, 0x0021, "DA", today_string);
/* Acquisition date */
add_tag_string (group_stream, group, 0x0022, "DA", today_string);
/* Content Date */
add_tag_string (group_stream, group, 0x0023, "DA", today_string);
/* Modality - I have to add something.. */
add_tag_string (group_stream, group, 0x0060, "CS", "MR");
write_group_to_file (DICOM, group, group_stream);
/* Patient group */
group = 0x0010;
/* Patient name */
add_tag_string (group_stream, group, 0x0010, "PN", "Wilber Doe");
/* Patient ID */
add_tag_string (group_stream, group, 0x0020, "CS", "314159265");
/* Patient Birth date */
add_tag_string (group_stream, group, 0x0030, "DA", today_string);
/* Patient sex */
add_tag_string (group_stream, group, 0x0040, "CS", "?");
write_group_to_file (DICOM, group, group_stream);
/* Relationship group */
group = 0x0020;
/* Instance number */
add_tag_string (group_stream, group, 0x0013, "IS", "1");
write_group_to_file (DICOM, group, group_stream);
/* Image presentation group */
group = 0x0028;
/* Samples per pixel */
add_tag_int (group_stream, group, 0x0002, "US", samples_per_pixel);
/* Photometric interpretation */
add_tag_string (group_stream, group, 0x0004, "CS", photometric_interp);
/* Planar configuration for color images */
if (samples_per_pixel == 3)
add_tag_int (group_stream, group, 0x0006, "US", 0);
/* rows */
add_tag_int (group_stream, group, 0x0010, "US", drawable->height);
/* columns */
add_tag_int (group_stream, group, 0x0011, "US", drawable->width);
/* Bits allocated */
add_tag_int (group_stream, group, 0x0100, "US", 8);
/* Bits Stored */
add_tag_int (group_stream, group, 0x0101, "US", 8);
/* High bit */
add_tag_int (group_stream, group, 0x0102, "US", 7);
/* Pixel representation */
add_tag_int (group_stream, group, 0x0103, "US", 0);
write_group_to_file (DICOM, group, group_stream);
/* Pixel data */
group = 0x7fe0;
{
guchar *src;
src = g_new (guchar,
drawable->height * drawable->width * samples_per_pixel);
gimp_pixel_rgn_init (&pixel_rgn, drawable,
0, 0, drawable->width, drawable->height,
FALSE, FALSE);
gimp_pixel_rgn_get_rect (&pixel_rgn,
src, 0, 0, drawable->width, drawable->height);
add_tag_pointer (group_stream, group, 0x0010, "OW", (guint8 *) src,
drawable->width * drawable->height * samples_per_pixel);
g_free (src);
}
retval = write_group_to_file (DICOM, group, group_stream);
fclose (DICOM);
g_byte_array_free (group_stream, TRUE);
gimp_drawable_detach (drawable);
return retval;
}
/* add_tag_pointer () adds to the group_stream one single value with its
* corresponding value_rep. Note that we use "explicit VR".
*/
static void
add_tag_pointer (GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
guint8 *data,
gint length)
{
gboolean is_long;
guint16 swapped16;
guint32 swapped32;
is_long = (strstr ("OB|OW|SQ|UN", value_rep) != NULL) || length > 65535;
swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (group));
g_byte_array_append (group_stream, (gchar*) &swapped16, 2);
swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (element));
g_byte_array_append (group_stream, (gchar*) &swapped16, 2);
g_byte_array_append (group_stream, value_rep, 2);
if (is_long)
{
g_byte_array_append (group_stream, "\0\0", 2);
swapped32 = g_ntohl (GUINT32_SWAP_LE_BE (length));
g_byte_array_append (group_stream, (gchar*) &swapped32, 4);
}
else
{
swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (length));
g_byte_array_append (group_stream, (gchar*) &swapped16, 2);
}
g_byte_array_append (group_stream, data, length);
}
/* Convenience function for adding a string to the dicom stream */
static void
add_tag_string (GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
gchar *s)
{
add_tag_pointer (group_stream,
group, element, value_rep, (guint8 *) s, strlen(s));
}
/* Convenience function for adding an integer to the dicom stream */
static void
add_tag_int(GByteArray *group_stream,
gint group,
gint element,
gchar *value_rep,
gint value)
{
gint len;
if (strcmp(value_rep, "US") == 0)
len = 2;
else
len = 4;
add_tag_pointer (group_stream,
group, element, value_rep, (guint8 *) &value, len);
}
/* Once a group has been built it has to be wrapped with a meta-group
* tag before it is written to the DICOM file. This is done by
* write_group_to_file.
*/
static gboolean
write_group_to_file (FILE *DICOM,
gint group,
GByteArray *group_stream)
{
gboolean retval = TRUE;
guint16 swapped16;
guint32 swapped32;
/* Add header to the group and output it */
swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (group));
fwrite ((gchar *) &swapped16, 1, 2, DICOM);
fputc (0, DICOM);
fputc (0, DICOM);
fputc ('U', DICOM);
fputc ('L', DICOM);
swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (4));
fwrite ((gchar *) &swapped16, 1, 2, DICOM);
swapped32 = g_ntohl (GUINT32_SWAP_LE_BE (group_stream->len));
fwrite ((gchar *) &swapped32, 1, 4, DICOM);
if (fwrite (group_stream->data,
1, group_stream->len, DICOM) != group_stream->len)
retval = FALSE;
g_byte_array_set_size (group_stream, 0);
return retval;
}