/* 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 #include #include #include #include #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_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 ", "2003", N_("DICOM image"), NULL, GIMP_PLUGIN, G_N_ELEMENTS (load_args), G_N_ELEMENTS (load_return_vals), load_args, load_return_vals); gimp_register_file_handler_mime ("file_dicom_load", "image/x-dcm"); gimp_register_magic_load_handler ("file_dicom_load", "dcm,dicom", "", "128,string,DICM" ); 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 ", "2003", N_("Digital Imaging and Communications in Medicine image"), "RGB, GRAY", GIMP_PLUGIN, G_N_ELEMENTS (save_args), 0, save_args, NULL); gimp_register_file_handler_mime ("file_dicom_save", "image/x-dcm"); gimp_register_save_handler ("file_dicom_save", "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; /* open the file */ DICOM = fopen (filename, "rb"); if (!DICOM) { g_message (_("Could not open '%s' for reading: %s"), gimp_filename_to_utf8 (filename), g_strerror (errno)); return -1; } temp = g_strdup_printf (_("Opening '%s'..."), gimp_filename_to_utf8 (filename)); gimp_progress_init (temp); g_free (temp); /* 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.", gimp_filename_to_utf8 (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."), gimp_filename_to_utf8 (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 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"), gimp_filename_to_utf8 (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; }