/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Pix
*
* Copyright (C) 2011-2015 Free Software Foundation, Inc.
*
* 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, see .
*/
#include
#include "jpeg-info.h"
void
_jpeg_info_data_init (JpegInfoData *data)
{
data->valid = _JPEG_INFO_NONE;
data->width = 0;
data->height = 0;
data->orientation = GTH_TRANSFORM_NONE;
data->icc_data = NULL;
data->icc_data_size = 0;
}
void
_jpeg_info_data_dispose (JpegInfoData *data)
{
if (data->valid & _JPEG_INFO_ICC_PROFILE)
g_free (data->icc_data);
}
static guchar
_g_input_stream_read_byte (GInputStream *stream,
GCancellable *cancellable,
GError **error)
{
guchar v;
return (g_input_stream_read (stream, &v, 1, cancellable, error) > 0) ? v : 0;
}
static guchar
_jpeg_read_segment_marker (GInputStream *stream,
GCancellable *cancellable,
GError **error)
{
guchar marker_id;
if (_g_input_stream_read_byte (stream, cancellable, error) != 0xff)
return 0x00;
while ((marker_id = _g_input_stream_read_byte (stream, cancellable, error)) == 0xff)
/* skip padding */;
return marker_id;
}
static gboolean
_jpeg_skip_segment_data (GInputStream *stream,
guchar marker_id,
GCancellable *cancellable,
GError **error)
{
if (marker_id == 0xd9) /* EOI => end of image */
return FALSE;
if (marker_id == 0xda) /* SOS => end of header */
return FALSE;
if ((marker_id != 0xd0)
&& (marker_id != 0xd1)
&& (marker_id != 0xd2)
&& (marker_id != 0xd3)
&& (marker_id != 0xd4)
&& (marker_id != 0xd5)
&& (marker_id != 0xd6)
&& (marker_id != 0xd7)
&& (marker_id != 0xd8)
&& (marker_id != 0x01))
{
guint h, l;
guint segment_size;
/* skip to the next segment */
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
segment_size = (h << 8) + l;
if (g_input_stream_skip (stream, segment_size - 2, cancellable, error) < 0)
return FALSE;
}
return TRUE;
}
static gboolean
_jpeg_exif_tags_from_app1_segment (guchar *in_buffer,
gsize app1_segment_size,
JpegInfoFlags flags,
JpegInfoData *data)
{
int pos;
guint length;
gboolean big_endian;
guchar *exif_data;
guint offset, number_of_tags, tagnum;
int remaining_tags;
/* Length includes itself, so must be at least 2 */
/* Following Exif data length must be at least 6 */
length = app1_segment_size;
if (length < 6)
return FALSE;
pos = 0;
/* Read Exif head, check for "Exif" */
if ((in_buffer[pos++] != 'E')
|| (in_buffer[pos++] != 'x')
|| (in_buffer[pos++] != 'i')
|| (in_buffer[pos++] != 'f')
|| (in_buffer[pos++] != 0)
|| (in_buffer[pos++] != 0))
{
return FALSE;
}
/* Length of an IFD entry */
if (length < 12)
return FALSE;
exif_data = in_buffer + pos;
/* Discover byte order */
if ((exif_data[0] == 0x49) && (exif_data[1] == 0x49))
big_endian = FALSE;
else if ((exif_data[0] == 0x4D) && (exif_data[1] == 0x4D))
big_endian = TRUE;
else
return FALSE;
/* Check Tag Mark */
if (big_endian) {
if (exif_data[2] != 0)
return FALSE;
if (exif_data[3] != 0x2A)
return FALSE;
}
else {
if (exif_data[3] != 0)
return FALSE;
if (exif_data[2] != 0x2A)
return FALSE;
}
/* Get first IFD offset (offset to IFD0) */
if (big_endian) {
if (exif_data[4] != 0)
return FALSE;
if (exif_data[5] != 0)
return FALSE;
offset = (exif_data[6] << 8) + exif_data[7];
}
else {
if (exif_data[7] != 0)
return FALSE;
if (exif_data[6] != 0)
return FALSE;
offset = (exif_data[5] << 8) + exif_data[4];
}
if (offset > length - 2) /* check end of data segment */
return FALSE;
/* Get the number of directory entries contained in this IFD */
if (big_endian)
number_of_tags = (exif_data[offset] << 8) + exif_data[offset+1];
else
number_of_tags = (exif_data[offset+1] << 8) + exif_data[offset];
if (number_of_tags == 0)
return FALSE;
offset += 2;
/* Search the tags in IFD0 */
remaining_tags = 0;
if (flags & _JPEG_INFO_EXIF_ORIENTATION)
remaining_tags += 1;
if (flags & _JPEG_INFO_EXIF_COLORIMETRY)
remaining_tags += 3;
if (flags & _JPEG_INFO_EXIF_COLOR_SPACE)
remaining_tags += 1;
for (;;) {
if (offset > length - 12) /* check end of data segment */
return FALSE;
/* Get Tag number */
if (big_endian)
tagnum = (exif_data[offset] << 8) + exif_data[offset+1];
else
tagnum = (exif_data[offset+1] << 8) + exif_data[offset];
if ((flags & _JPEG_INFO_EXIF_ORIENTATION) && (tagnum == 0x0112)) { /* Orientation */
int orientation;
if (big_endian) {
if (exif_data[offset + 8] != 0)
return FALSE;
orientation = exif_data[offset + 9];
}
else {
if (exif_data[offset + 9] != 0)
return FALSE;
orientation = exif_data[offset + 8];
}
if (orientation > 8)
orientation = 0;
data->orientation = orientation;
data->valid |= _JPEG_INFO_EXIF_ORIENTATION;
remaining_tags--;
}
if ((flags & _JPEG_INFO_EXIF_COLORIMETRY) && (tagnum == 0x012D)) { /* TransferFunction */
remaining_tags--;
}
if ((flags & _JPEG_INFO_EXIF_COLORIMETRY) && (tagnum == 0x013E)) { /* WhitePoint */
remaining_tags--;
}
if ((flags & _JPEG_INFO_EXIF_COLORIMETRY) && (tagnum == 0x013F)) { /* PrimaryChromaticities */
remaining_tags--;
}
if ((flags & _JPEG_INFO_EXIF_COLOR_SPACE) && (tagnum == 0xA001)) { /* ColorSpace */
int value;
if (big_endian) {
if (exif_data[offset + 8] != 0)
return FALSE;
value = exif_data[offset + 9];
}
else {
if (exif_data[offset + 9] != 0)
return FALSE;
value = exif_data[offset + 8];
}
if (value == 1)
data->color_space = GTH_COLOR_SPACE_SRGB;
else if (value == 0xFFFF)
data->color_space = GTH_COLOR_SPACE_UNCALIBRATED;
else
data->color_space = GTH_COLOR_SPACE_UNKNOWN;
data->valid |= _JPEG_INFO_EXIF_COLOR_SPACE;
remaining_tags--;
}
if (remaining_tags == 0)
break;
if (--number_of_tags == 0)
return FALSE;
offset += 12;
}
return TRUE;
}
/* -- _jpeg_get_icc_profile_chunk_from_app2_segment -- */
typedef struct {
int seq_n;
int tot;
guchar *in_buffer;
guchar *data;
gsize size;
} ICCProfileChunk;
static void
icc_profile_chunk_free (ICCProfileChunk *chunk)
{
g_free (chunk->in_buffer);
g_free (chunk);
}
static int
icc_chunk_compare (gconstpointer a,
gconstpointer b)
{
const ICCProfileChunk *chunk_a = a;
const ICCProfileChunk *chunk_b = b;
if (chunk_a->seq_n < chunk_b->seq_n)
return -1;
if (chunk_a->seq_n > chunk_b->seq_n)
return 1;
return 0;
}
static ICCProfileChunk *
_jpeg_get_icc_profile_chunk_from_app2_segment (guchar *in_buffer,
gsize app2_segment_size)
{
int pos;
guint length;
ICCProfileChunk *chunk;
length = app2_segment_size;
if (length <= 14)
return NULL;
pos = 0;
/* check for "ICC_PROFILE" */
if ((in_buffer[pos++] != 'I')
|| (in_buffer[pos++] != 'C')
|| (in_buffer[pos++] != 'C')
|| (in_buffer[pos++] != '_')
|| (in_buffer[pos++] != 'P')
|| (in_buffer[pos++] != 'R')
|| (in_buffer[pos++] != 'O')
|| (in_buffer[pos++] != 'F')
|| (in_buffer[pos++] != 'I')
|| (in_buffer[pos++] != 'L')
|| (in_buffer[pos++] != 'E')
|| (in_buffer[pos++] != 0))
{
return NULL;
}
chunk = g_new (ICCProfileChunk, 1);
chunk->in_buffer = in_buffer;
chunk->seq_n = in_buffer[pos++];
chunk->tot = in_buffer[pos++];
chunk->data = in_buffer + 14;
chunk->size = app2_segment_size - 14;
return chunk;
}
#define _JPEG_MARKER_SOF0 0xc0
#define _JPEG_MARKER_SOF1 0xc2
#define _JPEG_MARKER_APP1 0xe1
#define _JPEG_MARKER_APP2 0xe2
gboolean
_jpeg_info_get_from_stream (GInputStream *stream,
JpegInfoFlags flags,
JpegInfoData *data,
GCancellable *cancellable,
GError **error)
{
GList *icc_chunks;
guchar marker_id;
g_return_val_if_fail (data->valid == _JPEG_INFO_NONE, FALSE);
icc_chunks = NULL;
while ((marker_id = _jpeg_read_segment_marker (stream, cancellable, error)) != 0x00) {
gboolean segment_data_consumed = FALSE;
if (((flags & _JPEG_INFO_IMAGE_SIZE) && ! (data->valid & _JPEG_INFO_IMAGE_SIZE))
&& ((marker_id == _JPEG_MARKER_SOF0) || (marker_id == _JPEG_MARKER_SOF1)))
{
guint h, l;
guint size;
/* size */
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
size = (h << 8) + l;
/* data precision */
(void) _g_input_stream_read_byte (stream, cancellable, error);
/* height */
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
data->height = (h << 8) + l;
/* width */
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
data->width = (h << 8) + l;
g_input_stream_skip (stream, size - 7, cancellable, error);
segment_data_consumed = TRUE;
data->valid |= _JPEG_INFO_IMAGE_SIZE;
}
if (((flags & _JPEG_INFO_EXIF_ORIENTATION)
|| (flags & _JPEG_INFO_EXIF_COLORIMETRY)
|| (flags & _JPEG_INFO_EXIF_COLOR_SPACE))
&& (marker_id == _JPEG_MARKER_APP1))
{
guint h, l;
guint app1_segment_size;
guchar *app1_segment;
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
app1_segment_size = (h << 8) + l - 2;
app1_segment = g_new (guchar, app1_segment_size);
if (g_input_stream_read_all (stream,
app1_segment,
app1_segment_size,
NULL,
cancellable,
error))
{
_jpeg_exif_tags_from_app1_segment (app1_segment, app1_segment_size, flags, data);
}
segment_data_consumed = TRUE;
g_free (app1_segment);
}
if ((flags & _JPEG_INFO_ICC_PROFILE) && (marker_id == _JPEG_MARKER_APP2)) {
guint h, l;
gsize app2_segment_size;
guchar *app2_segment;
/* size */
h = _g_input_stream_read_byte (stream, cancellable, error);
l = _g_input_stream_read_byte (stream, cancellable, error);
app2_segment_size = (h << 8) + l - 2;
app2_segment = g_new (guchar, app2_segment_size);
if (g_input_stream_read_all (stream,
app2_segment,
app2_segment_size,
NULL,
cancellable,
error))
{
ICCProfileChunk *chunk;
chunk = _jpeg_get_icc_profile_chunk_from_app2_segment (app2_segment, app2_segment_size);
if (chunk != NULL)
icc_chunks = g_list_prepend (icc_chunks, chunk);
}
segment_data_consumed = TRUE;
}
if (! segment_data_consumed && ! _jpeg_skip_segment_data (stream, marker_id, cancellable, error))
break;
}
if (flags & _JPEG_INFO_ICC_PROFILE) {
gboolean valid_icc = (icc_chunks != NULL);
GOutputStream *ostream;
GList *scan;
int seq_n;
ostream = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
icc_chunks = g_list_sort (icc_chunks, icc_chunk_compare);
seq_n = 1;
for (scan = icc_chunks; scan; scan = scan->next) {
ICCProfileChunk *chunk = scan->data;
if (chunk->seq_n != seq_n) {
valid_icc = FALSE;
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Invalid ICC data");
break;
}
g_output_stream_write_all (ostream, chunk->data, chunk->size, NULL, cancellable, error);
seq_n++;
}
if (valid_icc && g_output_stream_close (ostream, NULL, NULL)) {
data->valid |= _JPEG_INFO_ICC_PROFILE;
data->icc_data = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (ostream));
data->icc_data_size = g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (ostream));
}
g_object_unref (ostream);
}
g_list_free_full (icc_chunks, (GDestroyNotify) icc_profile_chunk_free);
return (flags == data->valid);
}
gboolean
_jpeg_info_get_from_buffer (guchar *in_buffer,
gsize in_buffer_size,
JpegInfoFlags flags,
JpegInfoData *data)
{
GInputStream *stream;
gboolean result;
stream = g_memory_input_stream_new_from_data (in_buffer, in_buffer_size, NULL);
result = _jpeg_info_get_from_stream (stream, flags, data, NULL, NULL);
g_object_unref (stream);
return result;
}