/* GIMP - The GNU 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 3 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, see . */ /* * The dicom reading and writing code was written from scratch * by Dov Grobgeld. (dov.grobgeld@gmail.com). */ #include "config.h" #include #include #include #include #include #include #include "libgimp/stdplugins-intl.h" #define LOAD_PROC "file-dicom-load" #define SAVE_PROC "file-dicom-save" #define PLUG_IN_BINARY "file-dicom" #define PLUG_IN_ROLE "gimp-file-dicom" /* 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 { guint 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; gint bits_stored; gint high_bit; gboolean is_signed; } 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, GError **error); static gboolean save_image (const gchar *filename, gint32 image_ID, gint32 drawable_ID, GError **error); static void dicom_loader (guint8 *pix_buf, DicomInfo *info, GeglBuffer *buffer); 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, const gchar *value_rep, const guint8 *data, gint length); static GSList * dicom_add_tags (FILE *DICOM, GByteArray *group_stream, GSList *elements); static gboolean write_group_to_file (FILE *DICOM, gint group, GByteArray *group_stream); const GimpPlugInInfo PLUG_IN_INFO = { NULL, /* init_proc */ NULL, /* quit_proc */ query, /* query_proc */ run, /* run_proc */ }; MAIN () static void query (void) { static const GimpParamDef load_args[] = { { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, { GIMP_PDB_STRING, "filename", "The name of the file to load" }, { GIMP_PDB_STRING, "raw-filename", "The name of the file to load" } }; static const GimpParamDef load_return_vals[] = { { GIMP_PDB_IMAGE, "image", "Output image" } }; static const GimpParamDef save_args[] = { { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, { 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 (LOAD_PROC, "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 (LOAD_PROC, "image/x-dcm"); gimp_register_magic_load_handler (LOAD_PROC, "dcm,dicom", "", "128,string,DICM" ); gimp_install_procedure (SAVE_PROC, "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 (SAVE_PROC, "image/x-dcm"); gimp_register_save_handler (SAVE_PROC, "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; GError *error = NULL; INIT_I18N (); gegl_init (NULL, NULL); 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, LOAD_PROC) == 0) { image_ID = load_image (param[1].data.d_string, &error); 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; if (error) { *nreturn_vals = 2; values[1].type = GIMP_PDB_STRING; values[1].data.d_string = error->message; } } } else if (strcmp (name, SAVE_PROC) == 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 (PLUG_IN_BINARY, 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, &error)) { status = GIMP_PDB_EXECUTION_ERROR; if (error) { *nreturn_vals = 2; values[1].type = GIMP_PDB_STRING; values[1].data.d_string = error->message; } } } if (export == GIMP_EXPORT_EXPORT) gimp_image_delete (image_ID); } else { status = GIMP_PDB_CALLING_ERROR; } values[0].data.d_status = status; } /** * add_parasites_to_image: * @data: pointer to a GimpParasite to be attached to the image * specified by @user_data. * @user_data: pointer to the image_ID to which parasite @data should * be added. * * Attaches parasite to image and also frees that parasite **/ static void add_parasites_to_image (gpointer data, gpointer user_data) { GimpParasite *parasite = (GimpParasite *) data; gint32 *image_ID = (gint32 *) user_data; gimp_image_attach_parasite (*image_ID, parasite); gimp_parasite_free (parasite); } static gint32 load_image (const gchar *filename, GError **error) { gint32 volatile image_ID = -1; gint32 layer_ID; GeglBuffer *buffer; GSList *elements = NULL; FILE *DICOM; gchar buf[500]; /* buffer for random things like scanning */ DicomInfo *dicominfo; guint width = 0; guint height = 0; gint samples_per_pixel = 0; gint bpp = 0; gint bits_stored = 0; gint high_bit = 0; guint8 *pix_buf = NULL; gboolean is_signed = FALSE; guint8 in_sequence = 0; gimp_progress_init_printf (_("Opening '%s'"), gimp_filename_to_utf8 (filename)); DICOM = g_fopen (filename, "rb"); if (! DICOM) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for reading: %s"), gimp_filename_to_utf8 (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.", gimp_filename_to_utf8 (filename)); g_free (dicominfo); fclose (DICOM); return -1; } fread (buf, 1, 4, DICOM); /* This should be dicom */ if (g_ascii_strncasecmp (buf,"DICM",4) != 0) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, _("'%s' is not a DICOM file."), gimp_filename_to_utf8 (filename)); g_free (dicominfo); fclose (DICOM); return -1; } while (!feof (DICOM)) { guint16 group_word; guint16 element_word; gchar value_rep[3]; guint32 element_length; guint16 ctx_us; guint8 *value; guint32 tag; gboolean __attribute__((unused))do_toggle_endian = FALSE; gboolean implicit_encoding = FALSE; 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)); tag = (group_word << 16) | element_word; fread(value_rep, 2, 1, DICOM); value_rep[2] = 0; /* Check if the value rep looks valid. There probably is a better way of checking this... */ if ((/* Always need lookup for implicit encoding */ tag > 0x0002ffff && implicit_encoding) /* This heuristics isn't used if we are doing implicit encoding according to the value representation... */ || ((value_rep[0] < 'A' || value_rep[0] > 'Z' || value_rep[1] < 'A' || value_rep[1] > 'Z') /* I found this in one of Ednas images. It seems like a bug... */ && !(value_rep[0] == ' ' && value_rep[1])) ) { /* 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)); } /* Short length */ else { guint16 el16; fread (&el16, 1, 2, DICOM); element_length = g_ntohs (GUINT16_SWAP_LE_BE (el16)); } /* Sequence of items - just ignore the delimiters... */ if (element_length == 0xffffffff) { in_sequence = 1; continue; } /* End of Sequence tag */ if (tag == 0xFFFEE0DD) { in_sequence = 0; continue; } /* Sequence of items item tag... Ignore as well */ if (tag == 0xFFFEE000) continue; /* Even for pixel data, we don't handle very large element lengths */ if (element_length >= (G_MAXUINT - 6)) { g_message ("'%s' seems to have an incorrect value field length.", gimp_filename_to_utf8 (filename)); gimp_quit (); } /* 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); /* ignore everything inside of a sequence */ if (in_sequence) { g_free (value); continue; } /* Some special casts that are used below */ ctx_us = *(guint16 *) value; /* Recognize some critical tags */ if (group_word == 0x0002) { switch (element_word) { case 0x0010: /* transfer syntax id */ if (strcmp("1.2.840.10008.1.2", (char*)value) == 0) { do_toggle_endian = FALSE; implicit_encoding = TRUE; } else if (strcmp("1.2.840.10008.1.2.1", (char*)value) == 0) do_toggle_endian = FALSE; else if (strcmp("1.2.840.10008.1.2.2", (char*)value) == 0) do_toggle_endian = TRUE; break; } } else 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 0x0101: /* bits stored */ bits_stored = ctx_us; break; case 0x0102: /* high bit */ high_bit = ctx_us; break; case 0x0103: /* is pixel representation signed? */ is_signed = (ctx_us == 0) ? FALSE : TRUE; break; } } /* Pixel data */ if (group_word == 0x7fe0 && element_word == 0x0010) { pix_buf = value; } else { /* save this element to a parasite for later writing */ GimpParasite *parasite; gchar pname[255]; /* all elements are retrievable using gimp_get_parasite_list() */ g_snprintf (pname, sizeof (pname), "dcm/%04x-%04x-%s", group_word, element_word, value_rep); if ((parasite = gimp_parasite_new (pname, GIMP_PARASITE_PERSISTENT, element_length, value))) { /* * at this point, the image has not yet been created, so * image_ID is not valid. keep the parasite around * until we're able to attach it. */ /* add to our list of parasites to be added (prepending * for speed. we'll reverse it later) */ elements = g_slist_prepend (elements, parasite); } g_free (value); } } if ((bpp != 8) && (bpp != 16)) { g_message ("'%s' has a bpp of %d which GIMP cannot handle.", gimp_filename_to_utf8 (filename), bpp); gimp_quit (); } if ((width > GIMP_MAX_IMAGE_SIZE) || (height > GIMP_MAX_IMAGE_SIZE)) { g_message ("'%s' has a larger image size (%d x %d) than GIMP can handle.", gimp_filename_to_utf8 (filename), width, height); gimp_quit (); } if (samples_per_pixel > 3) { g_message ("'%s' has samples per pixel of %d which GIMP cannot handle.", gimp_filename_to_utf8 (filename), samples_per_pixel); gimp_quit (); } dicominfo->width = width; dicominfo->height = height; dicominfo->bpp = bpp; dicominfo->bits_stored = bits_stored; dicominfo->high_bit = high_bit; dicominfo->is_signed = is_signed; 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_image_get_default_new_layer_mode (image_ID)); gimp_image_insert_layer (image_ID, layer_ID, -1, 0); buffer = gimp_drawable_get_buffer (layer_ID); #if GUESS_ENDIAN if (bpp == 16) guess_and_set_endian2 ((guint16 *) pix_buf, width * height); #endif dicom_loader (pix_buf, dicominfo, buffer); if (elements) { /* flip the parasites back around into the order they were * created (read from the file) */ elements = g_slist_reverse (elements); /* and add each one to the image */ g_slist_foreach (elements, add_parasites_to_image, (gpointer) &image_ID); g_slist_free (elements); } g_free (pix_buf); g_free (dicominfo); fclose (DICOM); g_object_unref (buffer); return image_ID; } static void dicom_loader (guint8 *pix_buffer, DicomInfo *info, GeglBuffer *buffer) { guchar *data; gint row_idx; gint width = info->width; gint height = info->height; gint samples_per_pixel = info->samples_per_pixel; guint16 *buf16 = (guint16 *) pix_buffer; if (info->bpp == 16) { gulong pix_idx; guint shift = info->high_bit + 1 - info->bits_stored; /* Reorder the buffer; also shift the data so that the LSB * of the pixel data is at the LSB of the 16-bit array entries * (i.e., compensate for high_bit and bits_stored). */ for (pix_idx = 0; pix_idx < width * height * samples_per_pixel; pix_idx++) buf16[pix_idx] = g_htons (buf16[pix_idx]) >> shift; } data = g_malloc (gimp_tile_height () * width * samples_per_pixel); for (row_idx = 0; row_idx < height; ) { guchar *d = data; gint start; gint end; gint scanlines; gint i; start = row_idx; end = row_idx + gimp_tile_height (); end = MIN (end, height); scanlines = end - start; for (i = 0; i < scanlines; i++) { if (info->bpp == 16) { guint16 *row_start; gint col_idx; row_start = buf16 + (row_idx + i) * width * samples_per_pixel; for (col_idx = 0; col_idx < width * samples_per_pixel; col_idx++) { /* Shift it by 8 bits, or less in case bits_stored * is less than bpp. */ d[col_idx] = (guint8) (row_start[col_idx] >> (info->bits_stored - 8)); if (info->is_signed) { /* If the data is negative, make it 0. Otherwise, * multiply the positive value by 2, so that the * positive values span between 0 and 254. */ if (d[col_idx] > 127) d[col_idx] = 0; else d[col_idx] <<= 1; } } } else if (info->bpp == 8) { guint8 *row_start; gint col_idx; row_start = (pix_buffer + (row_idx + i) * width * samples_per_pixel); for (col_idx = 0; col_idx < width * samples_per_pixel; col_idx++) { /* Shift it by 0 bits, or more in case bits_stored is * less than bpp. */ d[col_idx] = row_start[col_idx] << (8 - info->bits_stored); if (info->is_signed) { /* If the data is negative, make it 0. Otherwise, * multiply the positive value by 2, so that the * positive values span between 0 and 254. */ if (d[col_idx] > 127) d[col_idx] = 0; else d[col_idx] <<= 1; } } } d += width * samples_per_pixel; } gegl_buffer_set (buffer, GEGL_RECTANGLE (0, row_idx, width, scanlines), 0, NULL, data, GEGL_AUTO_ROWSTRIDE); row_idx += scanlines; gimp_progress_update ((gdouble) row_idx / (gdouble) height); } g_free (data); gimp_progress_update (1.0); } /* 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++; } } typedef struct { guint16 group_word; guint16 element_word; gchar value_rep[3]; guint32 element_length; guint8 *value; gboolean free; } DICOMELEMENT; /** * dicom_add_element: * @elements: head of a GSList containing DICOMELEMENT structures. * @group_word: Dicom Element group number for the tag to be added to * @elements. * @element_word: Dicom Element element number for the tag to be added * to @elements. * @value_rep: a string representing the Dicom VR for the new element. * @value: a pointer to an integer containing the value for the * element to be created. * * Creates a DICOMELEMENT object and inserts it into @elements. * * Return value: the new head of @elements **/ static GSList * dicom_add_element (GSList *elements, guint16 group_word, guint16 element_word, const gchar *value_rep, guint32 element_length, guint8 *value) { DICOMELEMENT *element = g_slice_new0 (DICOMELEMENT); element->group_word = group_word; element->element_word = element_word; strncpy (element->value_rep, value_rep, sizeof (element->value_rep)); element->element_length = element_length; element->value = value; return g_slist_prepend (elements, element); } static GSList * dicom_add_element_copy (GSList *elements, guint16 group_word, guint16 element_word, gchar *value_rep, guint32 element_length, const guint8 *value) { elements = dicom_add_element (elements, group_word, element_word, value_rep, element_length, g_memdup (value, element_length)); ((DICOMELEMENT *) elements->data)->free = TRUE; return elements; } /** * dicom_add_element_int: * @elements: head of a GSList containing DICOMELEMENT structures. * @group_word: Dicom Element group number for the tag to be added to * @elements. * @element_word: Dicom Element element number for the tag to be added to * @elements. * @value_rep: a string representing the Dicom VR for the new element. * @value: a pointer to an integer containing the value for the * element to be created. * * Creates a DICOMELEMENT object from the passed integer pointer and * adds it to @elements. Note: value should be the address of a * guint16 for @value_rep == %US or guint32 for other values of * @value_rep * * Return value: the new head of @elements */ static GSList * dicom_add_element_int (GSList *elements, guint16 group_word, guint16 element_word, gchar *value_rep, guint8 *value) { guint32 len; if (strcmp (value_rep, "US") == 0) len = 2; else len = 4; return dicom_add_element (elements, group_word, element_word, value_rep, len, value); } /** * dicom_element_done: * @data: pointer to a DICOMELEMENT structure which is to be destroyed. * * Destroys the DICOMELEMENT passed as @data **/ static void dicom_element_done (gpointer data) { if (data) { DICOMELEMENT *e = data; if (e->free) g_free (e->value); g_slice_free (DICOMELEMENT, data); } } /** * dicom_elements_destroy: * @elements: head of a GSList containing DICOMELEMENT structures. * * Destroys the list of DICOMELEMENTs **/ static void dicom_elements_destroy (GSList *elements) { if (elements) g_slist_free_full (elements, dicom_element_done); } /** * dicom_destroy_element: * @elements: head of a GSList containing DICOMELEMENT structures. * @ele: a DICOMELEMENT structure to be removed from @elements * * Removes the specified DICOMELEMENT from @elements and Destroys it * * Return value: the new head of @elements **/ static GSList * dicom_destroy_element (GSList *elements, DICOMELEMENT *ele) { if (ele) { elements = g_slist_remove_all (elements, ele); if (ele->free) g_free (ele->value); g_slice_free (DICOMELEMENT, ele); } return elements; } /** * dicom_elements_compare: * @a: pointer to a DICOMELEMENT structure. * @b: pointer to a DICOMELEMENT structure. * * Determines the equality of @a and @b as strcmp * * Return value: an integer indicating the equality of @a and @b. **/ static gint dicom_elements_compare (gconstpointer a, gconstpointer b) { DICOMELEMENT *e1 = (DICOMELEMENT *)a; DICOMELEMENT *e2 = (DICOMELEMENT *)b; if (e1->group_word == e2->group_word) { if (e1->element_word == e2->element_word) { return 0; } else if (e1->element_word > e2->element_word) { return 1; } else { return -1; } } else if (e1->group_word < e2->group_word) { return -1; } return 1; } /** * dicom_element_find_by_num: * @head: head of a GSList containing DICOMELEMENT structures. * @group_word: Dicom Element group number for the tag to be found. * @element_word: Dicom Element element number for the tag to be found. * * Retrieves the specified DICOMELEMENT from @head, if available. * * Return value: a DICOMELEMENT matching the specified group,element, * or NULL if the specified element was not found. **/ static DICOMELEMENT * dicom_element_find_by_num (GSList *head, guint16 group_word, guint16 element_word) { DICOMELEMENT data = { group_word,element_word, "", 0, NULL}; GSList *ele = g_slist_find_custom (head,&data,dicom_elements_compare); return (ele ? ele->data : NULL); } /** * dicom_get_elements_list: * @image_ID: the image_ID from which to read parasites in order to * retrieve the dicom elements * * Reads all DICOMELEMENTs from the specified image's parasites. * * Return value: a GSList of all known dicom elements **/ static GSList * dicom_get_elements_list (gint32 image_ID) { GSList *elements = NULL; GimpParasite *parasite; gchar **parasites = NULL; gint count = 0; parasites = gimp_image_get_parasite_list (image_ID, &count); if (parasites && count > 0) { gint i; for (i = 0; i < count; i++) { if (strncmp (parasites[i], "dcm", 3) == 0) { parasite = gimp_image_get_parasite (image_ID, parasites[i]); if (parasite) { gchar buf[1024]; gchar *ptr1; gchar *ptr2; gchar value_rep[3] = ""; guint16 group_word = 0; guint16 element_word = 0; /* sacrificial buffer */ strncpy (buf, parasites[i], sizeof (buf)); /* buf should now hold a string of the form * dcm/XXXX-XXXX-AA where XXXX are Hex values for * group and element respectively AA is the Value * Representation of the element * * start off by jumping over the dcm/ to the first Hex blob */ ptr1 = strchr (buf, '/'); if (ptr1) { gchar t[15]; ptr1++; ptr2 = strchr (ptr1,'-'); if (ptr2) *ptr2 = '\0'; g_snprintf (t, sizeof (t), "0x%s", ptr1); group_word = (guint16) g_ascii_strtoull (t, NULL, 16); ptr1 = ptr2 + 1; } /* now get the second Hex blob */ if (ptr1) { gchar t[15]; ptr2 = strchr (ptr1, '-'); if (ptr2) *ptr2 = '\0'; g_snprintf (t, sizeof (t), "0x%s", ptr1); element_word = (guint16) g_ascii_strtoull (t, NULL, 16); ptr1 = ptr2 + 1; } /* and lastly, the VR */ if (ptr1) strncpy (value_rep, ptr1, sizeof (value_rep)); /* * If all went according to plan, we should be able * to add this element */ if (group_word > 0 && element_word > 0) { const guint8 *val = gimp_parasite_data (parasite); const guint len = gimp_parasite_data_size (parasite); /* and add the dicom element, asking to have it's value copied for later garbage collection */ elements = dicom_add_element_copy (elements, group_word, element_word, value_rep, len, val); } gimp_parasite_free (parasite); } } } } /* cleanup the array of names */ g_strfreev (parasites); return elements; } /** * dicom_remove_gimp_specified_elements: * @elements: GSList to remove elements from * @samples_per_pixel: samples per pixel of the image to be written. * if set to %3 the planar configuration for color images * will also be removed from @elements * * Removes certain DICOMELEMENTs from the elements list which are specific to the output of this plugin. * * Return value: the new head of @elements **/ static GSList * dicom_remove_gimp_specified_elements (GSList *elements, gint samples_per_pixel) { DICOMELEMENT remove[] = { /* Image presentation group */ /* Samples per pixel */ {0x0028, 0x0002, "", 0, NULL}, /* Photometric interpretation */ {0x0028, 0x0004, "", 0, NULL}, /* rows */ {0x0028, 0x0010, "", 0, NULL}, /* columns */ {0x0028, 0x0011, "", 0, NULL}, /* Bits allocated */ {0x0028, 0x0100, "", 0, NULL}, /* Bits Stored */ {0x0028, 0x0101, "", 0, NULL}, /* High bit */ {0x0028, 0x0102, "", 0, NULL}, /* Pixel representation */ {0x0028, 0x0103, "", 0, NULL}, {0,0,"",0,NULL} }; DICOMELEMENT *ele; gint i; /* * Remove all Dicom elements which will be set as part of the writing of the new file */ for (i=0; remove[i].group_word > 0;i++) { if ((ele = dicom_element_find_by_num (elements,remove[i].group_word,remove[i].element_word))) { elements = dicom_destroy_element (elements,ele); } } /* special case - allow this to be overwritten if necessary */ if (samples_per_pixel == 3) { /* Planar configuration for color images */ if ((ele = dicom_element_find_by_num (elements,0x0028,0x0006))) { elements = dicom_destroy_element (elements,ele); } } return elements; } /** * dicom_ensure_required_elements_present: * @elements: GSList to remove elements from * @today_string: string containing today's date in DICOM format. This * is used to default any required Dicom elements of date * type to today's date. * * Defaults DICOMELEMENTs to the values set by previous version of * this plugin, but only if they do not already exist. * * Return value: the new head of @elements **/ static GSList * dicom_ensure_required_elements_present (GSList *elements, gchar *today_string) { const DICOMELEMENT defaults[] = { /* Meta element group */ /* 0002, 0001 - File Meta Information Version */ { 0x0002, 0x0001, "OB", 2, (guint8 *) "\0\1" }, /* 0002, 0010 - Transfer syntax uid */ { 0x0002, 0x0010, "UI", strlen ("1.2.840.10008.1.2.1"), (guint8 *) "1.2.840.10008.1.2.1"}, /* 0002, 0013 - Implementation version name */ { 0x0002, 0x0013, "SH", strlen ("GIMP Dicom Plugin 1.0"), (guint8 *) "GIMP Dicom Plugin 1.0" }, /* Identifying group */ /* ImageType */ { 0x0008, 0x0008, "CS", strlen ("ORIGINAL\\PRIMARY"), (guint8 *) "ORIGINAL\\PRIMARY" }, { 0x0008, 0x0016, "UI", strlen ("1.2.840.10008.5.1.4.1.1.7"), (guint8 *) "1.2.840.10008.5.1.4.1.1.7" }, /* Study date */ { 0x0008, 0x0020, "DA", strlen (today_string), (guint8 *) today_string }, /* Series date */ { 0x0008, 0x0021, "DA", strlen (today_string), (guint8 *) today_string }, /* Acquisition date */ { 0x0008, 0x0022, "DA", strlen (today_string), (guint8 *) today_string }, /* Content Date */ { 0x0008, 0x0023, "DA", strlen (today_string), (guint8 *) today_string}, /* Content Time */ { 0x0008, 0x0030, "TM", strlen ("000000.000000"), (guint8 *) "000000.000000"}, /* AccessionNumber */ { 0x0008, 0x0050, "SH", strlen (""), (guint8 *) "" }, /* Modality */ { 0x0008, 0x0060, "CS", strlen ("MR"), (guint8 *) "MR" }, /* ConversionType */ { 0x0008, 0x0064, "CS", strlen ("WSD"), (guint8 *) "WSD" }, /* ReferringPhysiciansName */ { 0x0008, 0x0090, "PN", strlen (""), (guint8 *) "" }, /* Patient group */ /* Patient name */ { 0x0010, 0x0010, "PN", strlen ("DOE^WILBER"), (guint8 *) "DOE^WILBER" }, /* Patient ID */ { 0x0010, 0x0020, "LO", strlen ("314159265"), (guint8 *) "314159265" }, /* Patient Birth date */ { 0x0010, 0x0030, "DA", strlen (today_string), (guint8 *) today_string }, /* Patient sex */ { 0x0010, 0x0040, "CS", strlen (""), (guint8 *) "" /* unknown */ }, /* Relationship group */ /* StudyId */ { 0x0020, 0x0010, "IS", strlen ("1"), (guint8 *) "1" }, /* SeriesNumber */ { 0x0020, 0x0011, "IS", strlen ("1"), (guint8 *) "1" }, /* AcquisitionNumber */ { 0x0020, 0x0012, "IS", strlen ("1"), (guint8 *) "1" }, /* Instance number */ { 0x0020, 0x0013, "IS", strlen ("1"), (guint8 *) "1" }, { 0, 0, "", 0, NULL } }; gint i; /* * Make sure that all of the default elements have a value */ for (i=0; defaults[i].group_word > 0; i++) { if (dicom_element_find_by_num (elements, defaults[i].group_word, defaults[i].element_word) == NULL) { elements = dicom_add_element (elements, defaults[i].group_word, defaults[i].element_word, defaults[i].value_rep, defaults[i].element_length, defaults[i].value); } } return elements; } /* 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, GError **error) { FILE *DICOM; GimpImageType drawable_type; GeglBuffer *buffer; const Babl *format; gint width; gint height; GByteArray *group_stream; GSList *elements = NULL; gint group; GDate *date; gchar today_string[16]; gchar *photometric_interp; gint samples_per_pixel; gboolean retval = TRUE; guint16 zero = 0; guint16 seven = 7; guint16 eight = 8; guchar *src = NULL; drawable_type = gimp_drawable_type (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: format = babl_format ("Y' u8"); samples_per_pixel = 1; photometric_interp = "MONOCHROME2"; break; case GIMP_RGB_IMAGE: format = babl_format ("R'G'B' u8"); 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_t (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 = g_fopen (filename, "wb"); if (!DICOM) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for writing: %s"), gimp_filename_to_utf8 (filename), g_strerror (errno)); return FALSE; } buffer = gimp_drawable_get_buffer (drawable_ID); width = gegl_buffer_get_width (buffer); height = gegl_buffer_get_height (buffer); /* 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 (); elements = dicom_get_elements_list (image_ID); if (0/*replaceElementsList*/) { /* to do */ } else if (1/*insist_on_basic_elements*/) { elements = dicom_ensure_required_elements_present (elements,today_string); } /* * Set value of custom elements */ elements = dicom_remove_gimp_specified_elements (elements,samples_per_pixel); /* Image presentation group */ group = 0x0028; /* Samples per pixel */ elements = dicom_add_element_int (elements, group, 0x0002, "US", (guint8 *) &samples_per_pixel); /* Photometric interpretation */ elements = dicom_add_element (elements, group, 0x0004, "CS", strlen (photometric_interp), (guint8 *) photometric_interp); /* Planar configuration for color images */ if (samples_per_pixel == 3) elements = dicom_add_element_int (elements, group, 0x0006, "US", (guint8 *) &zero); /* rows */ elements = dicom_add_element_int (elements, group, 0x0010, "US", (guint8 *) &height); /* columns */ elements = dicom_add_element_int (elements, group, 0x0011, "US", (guint8 *) &width); /* Bits allocated */ elements = dicom_add_element_int (elements, group, 0x0100, "US", (guint8 *) &eight); /* Bits Stored */ elements = dicom_add_element_int (elements, group, 0x0101, "US", (guint8 *) &eight); /* High bit */ elements = dicom_add_element_int (elements, group, 0x0102, "US", (guint8 *) &seven); /* Pixel representation */ elements = dicom_add_element_int (elements, group, 0x0103, "US", (guint8 *) &zero); /* Pixel data */ group = 0x7fe0; src = g_new (guchar, height * width * samples_per_pixel); if (src) { gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, width, height), 1.0, format, src, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); elements = dicom_add_element (elements, group, 0x0010, "OW", width * height * samples_per_pixel, (guint8 *) src); elements = dicom_add_tags (DICOM, group_stream, elements); g_free (src); } else { retval = FALSE; } fclose (DICOM); dicom_elements_destroy (elements); g_byte_array_free (group_stream, TRUE); g_object_unref (buffer); return retval; } /** * dicom_print_tags: * @data: pointer to a DICOMELEMENT structure which is to be written to file * @user_data: structure containing state information and output parameters * * Writes the specified DICOMELEMENT to @user_data's group_stream member. * Between groups, flushes the group_stream to @user_data's DICOM member. */ static void dicom_print_tags(gpointer data, gpointer user_data) { struct { FILE *DICOM; GByteArray *group_stream; gint last_group; } *d = user_data; DICOMELEMENT *e = (DICOMELEMENT *) data; if (d->last_group >= 0 && e->group_word != d->last_group) { write_group_to_file (d->DICOM, d->last_group, d->group_stream); } add_tag_pointer (d->group_stream, e->group_word, e->element_word, e->value_rep,e->value, e->element_length); d->last_group = e->group_word; } /** * dicom_add_tags: * @DICOM: File pointer to which @elements should be written. * @group_stream: byte array used for staging Dicom Element groups * before flushing them to disk. * @elements: GSList container the Dicom Element elements from * * Writes all Dicom tags in @elements to the file @DICOM * * Return value: the new head of @elements **/ static GSList * dicom_add_tags (FILE *DICOM, GByteArray *group_stream, GSList *elements) { struct { FILE *DICOM; GByteArray *group_stream; gint last_group; } data = { DICOM, group_stream, -1 }; elements = g_slist_sort (elements, dicom_elements_compare); g_slist_foreach (elements, dicom_print_tags, &data); /* make sure that the final group is written to the file */ write_group_to_file (data.DICOM, data.last_group, data.group_stream); return elements; } /* 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, const gchar *value_rep, const guint8 *data, gint length) { gboolean is_long; guint16 swapped16; guint32 swapped32; guint pad = 0; 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, (guint8 *) &swapped16, 2); swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (element)); g_byte_array_append (group_stream, (guint8 *) &swapped16, 2); g_byte_array_append (group_stream, (const guchar *) value_rep, 2); if (length % 2 != 0) { /* the dicom standard requires all elements to be of even byte * length. this element would be odd, so we must pad it before * adding it */ pad = 1; } if (is_long) { g_byte_array_append (group_stream, (const guchar *) "\0\0", 2); swapped32 = g_ntohl (GUINT32_SWAP_LE_BE (length + pad)); g_byte_array_append (group_stream, (guint8 *) &swapped32, 4); } else { swapped16 = g_ntohs (GUINT16_SWAP_LE_BE (length + pad)); g_byte_array_append (group_stream, (guint8 *) &swapped16, 2); } g_byte_array_append (group_stream, data, length); if (pad) { /* add a padding byte to the stream * * From ftp://medical.nema.org/medical/dicom/2009/09_05pu3.pdf: * * Values with VRs constructed of character strings, except in * the case of the VR UI, shall be padded with SPACE characters * (20H, in the Default Character Repertoire) when necessary to * achieve even length. Values with a VR of UI shall be padded * with a single trailing NULL (00H) character when necessary * to achieve even length. Values with a VR of OB shall be * padded with a single trailing NULL byte value (00H) when * necessary to achieve even length. */ if (strstr ("UI|OB", value_rep) != NULL) { g_byte_array_append (group_stream, (guint8 *) "\0", 1); } else { g_byte_array_append (group_stream, (guint8 *) " ", 1); } } } /* 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; }