Skip to content

Commit

Permalink
Make parsing of JPEG comments more robust, handle ImageDescription sm…
Browse files Browse the repository at this point in the history
…arter

Signed-off-by: Lukas Stockner <[email protected]>
  • Loading branch information
lukasstockner committed Nov 7, 2024
1 parent 39c6d81 commit 06ad8d5
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 21 deletions.
7 changes: 7 additions & 0 deletions src/include/OpenImageIO/imageio.h
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,13 @@ OIIO_API std::string geterror(bool clear = true);
/// When nonzero, use the new "OpenEXR core C library" when available,
/// for OpenEXR >= 3.1. This is experimental, and currently defaults to 0.
///
/// - `int jpeg:com_attributes`
///
/// When nonzero, try to parse JPEG comment blocks as key-value attributes,
/// and only set ImageDescription if the parsing fails. Otherwise, always
/// set ImageDescription to the first comment block. Default is 1.
///
///
/// - `int limits:channels` (1024)
///
/// When nonzero, the maximum number of color channels in an image. Image
Expand Down
1 change: 1 addition & 0 deletions src/include/imageio_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ extern OIIO_UTIL_API int oiio_print_debug;
extern OIIO_UTIL_API int oiio_print_uncaught_errors;
extern int oiio_log_times;
extern int openexr_core;
extern int jpeg_com_attributes;
extern int limit_channels;
extern int limit_imagesize_MB;
extern int imagebuf_print_uncaught_errors;
Expand Down
47 changes: 33 additions & 14 deletions src/jpeg.imageio/jpeginput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/strutil.h>
#include <OpenImageIO/tiffutils.h>

#include "jpeg_pvt.h"
Expand Down Expand Up @@ -288,23 +289,41 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)
jpeg_decode_iptc((unsigned char*)m->data);
else if (m->marker == JPEG_COM) {
std::string data((const char*)m->data, m->data_length);
if (!m_spec.find_attribute("ImageDescription", TypeDesc::STRING))
m_spec.attribute("ImageDescription", data);
// Additional string metadata can be stored in JPEG files as
// comment markers in the form "key:value".
// Since the key might also commonly contain a colon, the
// heuristic to separate them here is that if there are multiple
// colons, the first one belongs to the key, the second one is the
// separator, and any further ones belong to the value.
// comment markers in the form "key:value" or "ident:key:value".
// If the string contains a single colon, we assume key:value.
// If there's multiple, we try splitting as ident:key:value and
// check if ident and key are reasonable (in particular, whether
// ident is a C-style identifier and key is not surrounded by
// whitespace). If ident passes but key doesn't, assume key:value.
auto separator = data.find(':');
if (separator != std::string::npos && separator > 0) {
auto second_separator = data.find(':', separator + 1);
if (second_separator != std::string::npos)
separator = second_separator;
std::string key = data.substr(0, separator);
if (!m_spec.find_attribute(key, TypeDesc::STRING))
m_spec.attribute(key, data.substr(separator + 1));
if (OIIO::get_int_attribute("jpeg:com_attributes")
&& (separator != std::string::npos && separator > 0)) {
std::string left = data.substr(0, separator);
std::string right = data.substr(separator + 1);
separator = right.find(':');
if (separator != std::string::npos && separator > 0) {
std::string mid = right.substr(0, separator);
std::string value = right.substr(separator + 1);
if (Strutil::string_is_identifier(left)
&& (mid == Strutil::trimmed_whitespace(mid))) {
// Valid parsing: left is ident, mid is key
std::string attribute = left + ":" + mid;
if (!m_spec.find_attribute(attribute, TypeDesc::STRING))
m_spec.attribute(attribute, value);
continue;
}
}
if (left == Strutil::trimmed_whitespace(left)) {
// Valid parsing: left is key, right is value
if (!m_spec.find_attribute(left, TypeDesc::STRING))
m_spec.attribute(left, right);
continue;
}
}
// If we made it this far, treat the comment as a description
if (!m_spec.find_attribute("ImageDescription", TypeDesc::STRING))
m_spec.attribute("ImageDescription", data);
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/libOpenImageIO/imageio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ atomic_int oiio_try_all_readers(1);
#endif
// Should we use "Exr core C library"?
int openexr_core(OIIO_OPENEXR_CORE_DEFAULT);
int jpeg_com_attributes(1);
int tiff_half(0);
int tiff_multithread(1);
int dds_bc5normal(0);
Expand Down Expand Up @@ -366,6 +367,10 @@ attribute(string_view name, TypeDesc type, const void* val)
openexr_core = *(const int*)val;
return true;
}
if (name == "jpeg:com_attributes" && type == TypeInt) {
jpeg_com_attributes = *(const int*)val;
return true;
}
if (name == "tiff:half" && type == TypeInt) {
tiff_half = *(const int*)val;
return true;
Expand Down Expand Up @@ -520,6 +525,10 @@ getattribute(string_view name, TypeDesc type, void* val)
*(int*)val = openexr_core;
return true;
}
if (name == "jpeg:com_attributes" && type == TypeInt) {
*(int*)val = jpeg_com_attributes;
return true;
}
if (name == "tiff:half" && type == TypeInt) {
*(int*)val = tiff_half;
return true;
Expand Down
66 changes: 59 additions & 7 deletions testsuite/jpeg-metadata/ref/out.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
ImageDescription: "Blender:File:<untitled>"
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Expand All @@ -18,19 +17,15 @@ Reading no-attribs.jpg
no-attribs.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "Blender:File:<untitled>"
Blender:File: "<untitled>"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "Blender:File:<untitled>"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
ImageDescription: "Blender:File:<untitled>"
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Expand All @@ -46,7 +41,6 @@ Reading with-attribs.jpg
with-attribs.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "Blender:File:<untitled>"
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Expand All @@ -57,6 +51,64 @@ with-attribs.jpg : 640 x 480, 3 channel, uint8 jpeg
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "Blender:File:<untitled>"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "with-attribs-and-desc.jpg"
PASS
Reading with-attribs-and-desc.jpg
with-attribs-and-desc.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "A photo"
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "A photo"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "with-colon-desc.jpg"
PASS
Reading with-colon-desc.jpg
with-colon-desc.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "Example:Text"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "Example:Text"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
15 changes: 15 additions & 0 deletions testsuite/jpeg-metadata/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@
output_filename="with-attribs.jpg",
extraargs="--attrib:type=int jpeg:com_attributes 1")
command += info_command ("with-attribs.jpg", safematch=True)

# Check that JPEG comments that don't match an attribute will be read as ImageDescription.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="with-attribs-and-desc.jpg",
extraargs="--attrib:type=int jpeg:com_attributes 1 "
"--attrib:type=string ImageDescription 'A photo'")
command += info_command ("with-attribs-and-desc.jpg", safematch=True)

# Check that JPEG comments that would match an attribute will be read as ImageDescription
# if jpeg:com_attributes is 0.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="with-colon-desc.jpg",
extraargs="--attrib:type=string ImageDescription 'Example:Text'")
command += info_command ("with-colon-desc.jpg", safematch=True,
extraargs="--oiioattrib:type=int jpeg:com_attributes 0")

0 comments on commit 06ad8d5

Please sign in to comment.