Image:...a::JPEG(3pm)

πŸ“¦
version 0.159 Β· 2 versions
  • 0.159 Β· debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking (latest)
  • 0.153 Β· ubuntu jammy
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– Image::MetaData::JPEG(3pm) πŸ’–

okay bestie, grab a boba, sit down, because we're about to get deep into JPEG metadata and honestly? it's giving main character energy. πŸŽ€


✨ NAME ✨

Image::MetaData::JPEG β€” a Perl extension for showing/modifying JPEG (meta)data. that's it, that's the whole vibe. πŸ¦‹


πŸ“Έ SYNOPSIS πŸ“Έ

use Image::MetaData::JPEG;

# Create a new JPEG file structure object
my $image = new Image::MetaData::JPEG('somepicture.jpg');
die 'Error: ' . Image::MetaData::JPEG::Error() unless $image;

# Get a list of references to comment segments
my @segments = $image->get_segments('COM', 'INDEXES');

# Get the JPEG picture dimensions
my ($dim_x, $dim_y) = $image->get_dimensions();

# Show all JPEG segments and their content
print $image->get_description();

# Retrieve a specific value from Exif meta-data
my $image_data = $image->get_Exif_data('IMAGE_DATA', 'TEXTUAL');
print $image_data->{DateTimeOriginal}->[0], "\n";

# Modify the DateTime tag for the main image
$image->set_Exif_data({'DateTime' => '1994:07:23 12:14:51'},
                       'IMAGE_DATA', 'ADD');

# Delete all meta-data segments (please, don't)
$image->drop_segments('METADATA');

# Rewrite file to disk after your modifications
$image->save('new_file_name.jpg');

# ... and a lot more methods for viewing/modifying meta-data, which
# are accessed through the $file or $segments[$index] references.

🌈 DESCRIPTION 🌈

so what is this module actually doing? the whole point is to read, modify and rewrite meta-data segments in JPEG (Joint Photographic Experts Group format) files. and those files can hold so much: comments, thumbnails, Exif information (that's your photographic parameters, iykyk), IPTC information (editorial parameters), and similar data. obsessed. πŸ’…

every single JPEG file is made of consecutive segments (tagged data blocks), plus the actual row picture data. most of those segments are just there to specify parameters for decoding the picture data into a bitmap. but some of them β€” namely the COMment and APPlication segments β€” contain instead meta-data, i.e., info about how the photo was shot (usually added by a digital camera) and extra notes from the photographer. these little extra pieces of info are especially valuable for picture databases, since the meta-data can be saved right alongside the picture without resorting to additional database structures. slay. see the appendix about the structure of JPEG files for the technical details. ✨

this module works by breaking a JPEG file into individual segments. each file gets associated to an Image::MetaData::JPEG structure object, which contains one Image::MetaData::JPEG::Segment object for each segment. segments with a known format then get parsed, and their content can be accessed in a structured way for display. some of them can even be modified and then rewritten to disk. no cap. πŸ”₯

🌸 $JPEG::show_warnings 🌸

this package variable is the one you use to inhibit the printing of warnings: if it's false, warnings are silently ignored. otherwise, warning messages come with a detailed back-trace and description of the warning location. very demure, very mindful.

$Image::MetaData::JPEG::show_warnings = undef;

πŸ‘‘ Managing a JPEG structure object πŸ‘‘

πŸ’… JPEG::new

[arguments: ($input, $regex, $options)]

first thing you need in order to interact with a JPEG picture is to create an Image::MetaData::JPEG structure object. that happens with a call to the new method, whose first argument is an input source β€” either a scalar, interpreted as a file name to be opened and read, or a scalar reference, interpreted as a pointer to an in-memory buffer containing a JPEG stream. this interface is similar to that of Image::Info, but no open file handle is (currently) accepted. 🫢

the constructor then parses the picture content and stores its segments internally. the memory footprint is close to the size of the disk file plus a few tens of kilobytes. cute and efficient.

my $file = new Image::MetaData::JPEG('a_file_name.jpg');
my $file = new Image::MetaData::JPEG(\ $a_JPEG_stream);

the constructor method accepts two optional arguments: a regular expression and an option string. if the regular expression is present, it gets matched against segment names, and only those segments with a positive match are parsed (they are nonetheless stored). this allows for some speed-up if you just need partial information β€” but be sure not to miss something necessary; e.g., SOF segments are needed for reading the picture dimensions. for instance, if you just want to manipulate the comments, you could set the string to 'COM'. πŸ‘‘

my $file = new Image::MetaData::JPEG('a_file_name.jpg', 'COM');

the third optional argument is an option string. if it matches the string 'FASTREADONLY', only the segments matching the regular expression are actually stored; also, everything which is found after a Start Of Scan is completely neglected. this allows for very large speed-ups, but, obviously, you cannot rebuild the file afterwards, so this is only for getting information fast, e.g., when doing a directory scan. ⚑

my $file = new Image::MetaData::JPEG('a_file.jpg', 'COM', 'FASTREADONLY');
Nota bene 🫒: an old version of "Arles Image Web Page Creator" had a bug which caused the application to generate JPEG's with illegal comment segments, reportedly due to a bug in the Intel JPEG library the developers used at that time (these segments had to 0x00 bytes appended). it is true that a JPEG file with garbage between segments is to be considered invalid, but some libraries like IJG's try to forgive, so this module tries to forgive too, if the amount of garbage isn't too large (only a warning is printed). we're nice like that. πŸ’œ

πŸŽ€ JPEG::Error

[arguments: none]

if the file reference remains undefined after a call to new, the file is to be considered not parseable by this module, and one should issue some error message and go to another file. an error message explaining the reason of the failure can be retrieved with the Error method:

die 'Error: ' . Image::MetaData::JPEG::Error() unless $file;

πŸ¦‹ JPEG::get_segments

[arguments: ($regex, $do_indexes)]

if the new call is successful, the returned reference points to an Image::MetaData::JPEG structure object containing a list of references to Image::MetaData::JPEG::Segment objects, which can be retrieved with the get_segments method. 🌸

this method returns a list containing the references (or their indexes in the Segment references' list, if the second argument is the string INDEXES) to those Segments whose name matches the $regex regular expression. for instance, if $regex is 'APP', all application Segments will be returned. if you want only APP1 Segments you need to specify '^APP1$'. the output can become invalid after adding/removing any Segment. if $regex is undefined, all references are returned.

my @segments = $file->get_segments($regex, $do_indexes);

πŸ’– JPEG::drop_segments

[arguments: ($regex)]

similarly, if you are only interested in eliminating some segments, you can use the drop_segments method, which erases from the internal segment list all segments matching a given regular expression. ⚠️ if the regular expression is undefined or evaluates to the empty string, this method throws an exception, because I don't want the user to erase the whole file just because he/she did not understand what he was doing. one should also remember that it is not wise to drop non-meta-data segments, because this in general invalidates the file. as a special case, if $regex == 'METADATA', all APP* and COM segments are erased.

$file->drop_segments('^APP1$');

✨ JPEG::insert_segments

[arguments: ($segref, $pos, $overwrite)]

inserting a Segment into the picture's segment list is done with the insert_segments method. this method inserts the segments referenced by $segref into the current list of segments at position $pos. if $segref is undefined, the method fails silently. if $pos is undefined, the position is chosen automatically (using find_new_app_segment_position); if $pos is out of bound, an exception is thrown; this happens also if $pos points to the first segment, and it is an SOI. 🫒

$segref may be a reference to a single segment or a reference to a list of segment references; everything else throws an exception. if $overwrite is defined, it must be the number of segments to overwrite during the splice.

$file->insert_segments([$my_comment_1, $my_comment_2], 3, 1);

🌈 JPEG::get_description

🌈 JPEG::get_dimensions

[arguments: none]

getting a string describing the findings of the parsing stage is as easy as calling the get_description method. those Segments whose parsing failed have the first line of their description stating the stopping error condition. non-printable characters are replaced, in the string returned by get_description, by a slash followed by the two digit hexadecimal code of the character. πŸ”

the (x,y) dimensions of the JPEG picture are returned by get_dimensions from the Start of Frame (SOF*) Segment:

print $file->get_description();
my ($dim_x, $dim_y) = $file->get_dimensions();

πŸ’… JPEG::find_new_app_segment_position

[arguments: ($name)]

if a new comment or application Segment is to be added to the file, the module provides a standard algorithm for deciding the location of the new Segment, in the find_new_app_segment_position method. the argument is the name of the Segment to be inserted (it defaults to 'COM', producing a warning). πŸ‘‘

the position is chosen immediately before the first (or after the last) element of some list, provided that the list is not empty, otherwise the next list is taken into account:

  1. [for COM segments only] after 'COM' segments; otherwise after APP segments;
  2. [for APPx segments only] after APPy's (trying y = x..0, in sequence); otherwise before APPy's (trying y = x+1..15, in sequence);
  3. before DHP segments;
  4. before SOF segments.

if all these tentatives fail, the position immediately after the SOI segment is returned (i.e., 1). periodt. πŸ’…

my $new_position = $file->find_new_app_segment_position('APP2');

πŸ’Ύ JPEG::save πŸ’Ύ

[arguments: ($filename)]

Okay so here's the tea, bestie: every Segment inside your in-memory JPEG structure object has its own little data area, right? And you can rewrite all of those back out β€” to an actual file on disk, or to an in-memory scalar β€” which literally recreates the (possibly modified!) JPEG picture. Iconic. That's what the save method is for. It takes a filename or a scalar reference as its argument. And if the file name is undef? No worries, it just defaults to the file that was originally used to create the JPEG structure object. Slay.

It returns "true" (1) if it worked, and "false" (undef) if it didn't. That's it.

⚠️ But heads up: if the file was originally opened with the 'FASTREADONLY' option, saving is literally impossible β€” the call fails immediately. No cap.
print "Creation of $newJPEG failed!" unless $file->save($newJPEG);

Now, an example of how to use that in-memory magic proficiently to read the content of a JPEG thumbnail (peep later for get_Exif_data, and also, please, do some error checking!):

my $thumbnail = $file->get_Exif_data('THUMBNAIL');
print Image::MetaData::JPEG->new($thumbnail)->get_description();

πŸŽ€ Managing a JPEG Segment object πŸŽ€

JPEG::Segment::name

JPEG::Segment::error

So, an Image::MetaData::JPEG::Segment object gets created for each Segment found in the JPEG image while the JPEG object is being created (see JPEG::new), and a parser routine runs at the same time. The name member of a Segment object tells you the "nature" of that Segment (like 'APP0', ..., 'APP15', or 'COM').

And if literally any error happens β€” whether it's in the Segment itself or in some underlying class β€” the parsing of that Segment gets interrupted at some point and stays incomplete. So the error member of that Segment object gets set to a meaningful error message. And if no error occurs? Same variable is just left undef. Chill.

printf 'Invalid %s!\n', $segment->{name} if $segment->{error};

JPEG::Segment::records

The reference to the Segment object is returned in any case. That's the whole point, bestie: a faulty Segment can't inhibit the creation of a JPEG structure object. Now β€” faulty segments can't be edited or modified, basically because their structure could not be fully understood. So they're always rewritten to disk unmodified, which means a file with corrupted or non-standard Segments can be partially edited without you having to fear damaging it. Whew. 🫢

Once a Segment has successfully been built, its parsed information is right there for you through the records member: it's a reference to an array of JPEG::Record objects, which is an internal class modelled on Exif records (peep the subsection about record management for the details, iykyk).

my $records = $segment->{records};
printf '%s has %d records\n', $segment->{name}, scalar @$records;

JPEG::Segment::search_record

JPEG::Segment::search_record_value

[arguments: ([$dirref], $keys ...)]

Need one specific record? Use search_record. It hunts for a record with a given key (see "JPEG::Record::key") inside a given record directory, and returns a reference to the record if the search was fruitful β€” or undef if it wasn't. πŸ’…

The algorithm goes like this:

  1. A start directory gets chosen by looking at the last argument: if it's an ARRAY ref, it gets popped out and used; otherwise the top-level directory is selected.
  2. A string gets created by joining all the remaining arguments on '@', then it's exploded into a list of keys on that same character (all undef or "false" arguments are simply discarded).
  3. Those keys are used for an iterative search starting from the initially chosen directory: all but the last key must correspond to $REFERENCE records. And if $key is exactly "FIRST_RECORD" / "LAST_RECORD", the first/last record in the current dir is used.
my $segments = $file->get_segments('APP0');
my $segment = $$segments[0];
print "I found it!\n" if $segment->search_record('Identifier');

If you only care about the Record's <em>value</em>, use search_record_value β€” it's a simple wrapper around search_record(): it returns the record value (with "JPEG::Record::get_value") if the search is successful, and undef otherwise.

print "Its value is: ", $segment->search_record_value('Identifier');
πŸ“Œ Nota bene: the returned record is initialised with a "fake" $REFERENCE record pointing to the records member of the current segment. That's why this record is what you get back if search_record is invoked with no arguments at all. And for the exact same reason, search_record_value invoked without arguments returns the records member:
$segment->search_record_value() eq $this->{records} || print "error!";

JPEG::Segment::update

[arguments: none]

So if you modify a Segment's content (aka its Records' values), you have to dump it into the private binary data area of the Segment, otherwise your change won't be written to disk at "JPEG::save" time. That's what update does.

It's only necessary if you changed record values "by hand", by the way β€” all the "high-level" methods for changing a Segment's content call "update" on their own. πŸ’…

But. Only Segments without errors can be updated (and don't you dare try to undef the Segment's error flag unless you know exactly what you're doing!). Trying to update a segment with errors throws an exception. Same thing happens when you try to update a segment without update support, or without records β€” that catches segments created with the 'NOPARSE' flag. So in practice: never use this method unless you're writing an extension for this module. Periodt.

πŸ’€ Note that this method preliminarily saves a reference to the old segment data area and restores it if the update process fails (and if that happens, a warning is generated). One wonders wheather there are there cleverer ways to handle this case (any suggestion is welcome). Honestly though? It's better to have a corrupt object in memory than a corrupt object written over the original. Currently, this is restricted to the possibility that an updated segment becomes too large.
$segment->update();

JPEG::Segment::reparse_as

[arguments: ($new_name)]

reparse_as re-executes the parsing of a Segment after changing the Segment name. Super handy if you've got a JPEG file with a "correct" application Segment β€” except its name is wrong. πŸ™ˆ

I used it the first time for a file that had an ICC_profile Segment (normally in APP2) stored as APP13. And heads up: the name of the Segment is permanently changed. So if the Segment gets updated and the file gets rewritten to disk, it'll be "correct". Slay.

for my $segment ($file->get_segments('APP13')) {
    $segment->reparse_as('APP2') if $segment->{error} &&
         $segment->search_record('Identifier') =~ 'ICC_PROFILE';
    $segment->update(); }

JPEG::Segment::output_segment_data

[arguments: none]

The current in-memory data area of a Segment can be output to a file through output_segment_data. (Exception made for entropy coded Segments, this includes the initial two bytes with the Segment identifier and the two bytes with the length, if present.) The argument is a file handle β€” and yeah, this is likely to become more general in the future.

If there are problems at output time (e.g., the segment content is too large), an exception is thrown.

eval { $segment->output_segment_data($output_handle) } ||
    print "A terrible output error occurred! Help me.\n";

JPEG::Segment::get_description

JPEG::Segment::size

[arguments: none]

You get a string describing the parsed content of the Segment through get_description β€” and yes, it's the same string used by the get_description method of a JPEG structure object. And if the Segment parsing stage got interrupted, this string includes the relevant error. 😭

Meanwhile size returns the size of the internal data area of a Segment object. Just so you know, this can be different from the length of the scalar returned by get_segment_data, because the identifier and the length are not included.

print $segment->get_description();
print 'Size is 4 + ' . $segment->size();

🌈 Managing a JPEG Record object 🌈

JPEG::Record::key

JPEG::Record::type

JPEG::Record::values

JPEG::Record::extra

The JPEG::Record class is an internal class for storing parsed information about a JPEG Segment, inspired by Exif records. A Record is made up by four fields: key, type, values and extra.

  • key is the record's identifier; it's either numeric or textual (numeric keys can be translated with the help of the %JPEG_lookup function in Tables.pm, included in this package).
  • type is obviously the type of stored info (like unsigned integers, ASCII strings and so on ...).
  • extra is a helper field for storing additional information.
  • And last but never least, values is an array reference to the record content (almost always there's just one value).

For instance, for a non-IPTC Photoshop record in APP13:

printf 'The numeric key 0x%04x means %s',
  $record->{key}, JPEG_lookup('APP13@Photoshop_RECORDS', $record->{key});
printf 'This record contains %d values\n', scalar @{$record->{values}};

A Record's type can be one among the following predefined constants β€” memorise these, bestie:

 0  $NIBBLES    two 4-bit unsigned integers (private)
 1  $BYTE       An 8-bit unsigned integer
 2  $ASCII      A variable length ASCII string
 3  $SHORT      A 16-bit unsigned integer
 4  $LONG       A 32-bit unsigned integer
 5  $RATIONAL   Two LONGs (numerator and denominator)
 6  $SBYTE      An 8-bit signed integer
 7  $UNDEF      A generic variable length string
 8  $SSHORT     A 16-bit signed integer
 9  $SLONG      A 32-bit signed integer (2's complement)
10  $SRATIONAL  Two SLONGs (numerator and denominator)
11  $FLOAT      A 32-bit float (a single float)
12  $DOUBLE     A 64-bit float (a double float)
13  $REFERENCE  A Perl list reference (internal)

$UNDEF is your go-to for binary data that nobody bothered to specify better. A numeric-type record can have multiple elements chillin' in its @{values} list (and yeah, $NIBBLES implies an even number, that's just how it is). An $UNDEF or $ASCII type record? Only one element, but its length can vary. And last but not least, a $REFERENCE record holds a single Perl reference to another record list β€” which, slay, is literally how you build a little directory tree inside a Segment. 🌳

JPEG::Record::get_category

[arguments: none] Wanna know what kind of record you're dealing with? Call get_category, bestie. It hands you back:

  • 'p' for Perl references
  • 'I' for integer types
  • 'S' for $ASCII and $UNDEF
  • 'R' for rational types
  • 'F' for floating point types

Like so:

for my $record (@{$segment->{records}}) {
    print "Subdir found\n" if $record->get_category() eq 'p'; }

JPEG::Record::get_description

[arguments: "($names)"] Want a human-readable lil' summary of what's inside a Record? That's the get_description method's whole job. Its argument is a reference to an array of names, and those names get used as successive keys into a general hash that keeps translations of numeric tags. If your key is already non-numeric, no argument needed (peep the get_value example for the deets).

In the output of get_description, unreasonably long strings get trimmed (rude, but necessary βœ‚οΈ), and non-printing characters get swapped out for their hexadecimal representation. Then strings get wrapped in delimiters, and null-terminated $ASCII strings have their last character chopped off β€” but a dot gets added after the closing delimiter, so it's fine. $ASCII strings use a " as delimiter, while $UNDEF strings use '.

print $record->get_description($names);

JPEG::Record::get_value

[arguments: "($index)"] When there's no "high-level" routine around to collect your info, you can read a Record's content directly β€” either by poking at the values member or by calling get_value. It returns the $index-th value in the value list; if the index is undefined (aka you didn't pass one), you get the sum/concatenation of all the values back. And yes, the index gets checked for out-of-bound errors, we're not chaotic like that.

Here's the code, an abridged version of get_description, showing you how to use these methods and members like a pro:

sub show_directory {
  my ($segment, $records, $names) = @_;
  my @subdirs = ();
  for my $record (@$records) {
    print $record->get_description($names);
    push @subdirs, $record if $record->get_category() eq 'p'; }
  foreach my $subdir (@subdirs) {
    my $directory = $subdir->get_value();
    push @$names, $subdir->{key};
    printf 'Subdir %s (%d records)', $names, scalar @$directory;
    show_directory($segment, $directory, $names);
    pop @$names; } }
show_directory($segment, $segment->{records}, [ $segment->{name} ]);

JPEG::Record::get

[arguments: "($endianness)"] Need the Record structure in full detail? Resort to the get method, babes. In list context it returns (key, type, count, dataref). That data reference points to a packed scalar, ready to be written straight to disk. In scalar context, it returns the dereferenced dataref β€” which is tricky, but handy for other routines, iykyk. The argument specifies an endianness (defaults to big endian, no cap).

my ($key, $type, $count, $dataref) = $record->get();

πŸ’¬ Comments ("COM" segments) πŸ’¬

JPEG::get_number_of_comments

JPEG::get_comments

[arguments: none] Every "COM" Segment in a JPEG file holds a user comment, and the content is free format β€” go wild. There's one lil' limitation tho: a JPEG Segment can't be longer than 64KB, which caps your comment at $max_length = (2^16 - 3) bytes.

get_number_of_comments gives you how many comment Segments are in the file, and get_comments returns a list of strings (each string is the content of a COM Segment). No comments present? They return zero and the empty list respectively. Cute, consistent. ✨

my $number = $file->get_number_of_comments();
my @comments = $file->get_comments();

JPEG::add_comment

[arguments: "($string)"] Adding a comment is add_comment's moment, and its only argument is a string. If that string is too long, it gets broken into multiple strings with length smaller or equal to $max_length, and multiple comment Segments get added to the file. If there's already at least one comment Segment, the new Segments are created right after the last one. Otherwise, the standard position search of find_new_app_segment_position is applied.

$file->add_comment('a' x 100000);

JPEG::set_comment

[arguments: "($index, $string)"] Got an existing comment you wanna replace? set_comment said hi. Its two arguments are an $index and a $string: the $index-th comment Segment gets replaced with one or more new Segments based on $string (and heads up, the index of the first comment Segment is 0). If $string is too big, it gets broken down just like in add_comment. If $string is undefined, the selected comment Segment gets erased. And if $index is out-of-bound, a warning gets printed out. πŸ‘€

$file->set_comment(0, 'This is the new comment');

JPEG::remove_comment

JPEG::remove_all_comments

[arguments: "($index)" for remove_comment] Only need to erase the comment? Just call remove_comment with the Segment $index and you're done. Wanna nuke them all? Call remove_all_comments. Periodt.

$file->remove_comment(0);
$file->remove_all_comments();

JPEG::join_comments

[arguments: "($separation, @selection)"] So apparently some JPEG comment readers out there just... don't read past the first comment?? Tragic. Which is exactly why join_comments exists β€” and its goal is pretty obvious from the name. πŸ’…

This method builds a string by joining all the comments selected by the @selection index list (the $separation scalar is a string inserted at each junction point), then overwrites the first selected comment while deleting the others. An exception gets thrown for each illegal comment index. Same string-length considerations as before apply. No separation string provided? It defaults to \n. No index in @selection? Then it assumes the method must join all the comments into the first one, and delete the others.

$file->join_comments('---', 2, 5, 8);

πŸ–ΌοΈ JFIF data ("APP0" segments) πŸ–ΌοΈ

APP0 Segments are written by older cameras that adopted the JFIF (JPEG File Interchange Format), or one of its extensions, for storing images. JFIF files use the APP0 application Segment to insert configuration data plus a JPEG or RGB packed thumbnail image. The format is described in the appendix about the APP0 structure, including the names of all possible tags. πŸ“Ž

And yes, of course you can access each APP0 Segment individually using the get_segments and search_record_value methods. Here's a snippet for doing exactly that:

for my $segment ($file->get_segments('APP0')) {
    my $iden = $segment->search_record_value('Identifier');
    my $xdim = $segment->search_record_value('Xthumbnail');
    my $ydim = $segment->search_record_value('Ythumbnail');
    printf 'Segment type: %s; dimensions: %dx%d\n',
            substr($iden, 0, -1), $xdim, $ydim;
    printf '%15s => %s\n', $_->{key}, $_->get_value()
            for $segment->{records}; }

JPEG::get_app0_data

[arguments: none] But if you'd rather not deal directly with Segments, you can use get_app0_data, which returns a reference to a hash with a plain translation of the content of the first interesting APP0 segment (this is the first 'JFXX' APP0 segment if present, otherwise the first 'JFIF' APP0 segment). Segments with errors are excluded. An empty hash means no valid APP0 segment is present. 🫢

my $data = $file->get_app0_data();
printf '%15s => %s\n', $_, (($_=~/..Thumbnail/)?'...':$$data{$_});

πŸ“Έ Exif data ("APP1" segments) πŸ“Έ

The DCT Exif (Exchangeable Image File format) standard serves up photographic meta-data in the APP1 section. Various tag-values pairs are stored in groups called IFDs (Image File Directories), and each group refers to a different kind of information β€” you can find data about how the photo was shot, GPS data, thumbnail data and so on... (see the appendix about the APP1 segment structure for more details). This module provides a bunch of methods for managing Exif data without you having to deal with the low-level representation details. 😌

Note that, given the complicated structure of an Exif APP1 segment (where extensive use of "pointers" is made), some digital cameras and graphic programs decide to leave some unused space in the JPEG file. The dump routines of this module, on the other hand, leave no unused space, so just calling update() on an Exif APP1 segment even without modifying its content can give you a smaller file (some tens of kilobytes can be saved). Free wins, bestie. πŸ”₯

JPEG::retrieve_app1_Exif_segment

[arguments: "($index)"] To work on Exif data, you gotta select an Exif APP1 Segment first. The retrieve_app1_Exif_Segment method returns a reference to the $index-th such Segment (the first Segment if the index is undefined). If no such Segment exists, the method returns the undefined reference. And if $index is (-1), the routine returns the number of available APP1 Exif Segments (which is non negative). Neat lil' trick.

my $num = $file->retrieve_app1_Exif_segment(-1);
my $ref = $file->retrieve_app1_Exif_segment($num - 1);

JPEG::provide_app1_Exif_segment

[arguments: none] Wanna be sure you have an Exif APP1 Segment? Use provide_app1_Exif_segment instead β€” it forces the Segment to be present in the file, and returns its reference. The algorithm goes like this:

  1. If at least one Segment with these properties is already present, we are done. βœ…
  2. If [1] fails, an APP1 segment is added and initialised with a big-endian Exif structure (its position is chosen by find_new_app_segment_position, as usual).

Also note: there's no $index argument here. πŸ™…β€β™€οΈ

my $ref = $file->provide_app1_Exif_segment();

πŸ’… JPEG::remove_app1_Exif_info

[arguments: "($index)"] okay so if you want to yeet the $index-th Exif APP1 Segment out of the JPEG file segment list, remove_app1_Exif_info is your girl. Same vibe as always, bestie: if $index is (-1), then every single Exif APP1 Segment gets affected at once; and if $index is undefined, it just defaults to -1 β€” so both (-1) and undef nuke all the Exif APP1 segments together. 🧨

⚠️ Heads up though: after this, the file won't be a valid Exif file. Like, at all. Not a drill.
    $file->remove_app1_Exif_info(-1);

πŸ” How to inspect your Exif data πŸ”

JPEG::Segment::get_Exif_data / JPEG::get_Exif_data

[arguments: "($what, $type)"] so once you've got a Segment reference pointing at your fave Exif Segment, you're probably dying to peek at the records inside it. Enter get_Exif_data β€” it takes two arguments ($what and $type) and hands you back the content of the APP1 segment packed in various forms. ✨

And the error handling? Very demure, very mindful: invalid $what's and $type's show up as an undefined return value. No drama, just undef.

So all Exif records are natively identified by numeric tags (keys), which you can "translate" into human-readable form using the Exif standard docs πŸ“š β€” but note that only a few fields in the Exif APP1 preamble (and those aren't even Exif records, bestie) are always identified by this module via textual tags. The $type argument picks the output format for the record keys (tags):

  • NUMERIC β€” record tags are native numeric keys
  • TEXTUAL β€” record tags are human-readable (this is the default, iykyk)

And obviously, record values are never translated. Periodt. If a numeric Exif tag isn't known, a custom textual key gets created with "Unknown_tag_" followed by its numerical value β€” which totally solves the whole non-standard-tags situation. 🫢

The subset of Exif tags you get back is decided by $what, which can be one of:

  • ALL β€” (default) everything but THUMBNAIL β€” ref. to hash of hashes
  • IMAGE_DATA β€” a merge of IFD0_DATA and SUBIFD_DATA β€” ref. to flat hash
  • THUMB_DATA β€” this is an alias for IFD1_DATA β€” ref. to flat hash
  • THUMBNAIL β€” the actual (un)compressed thumbnail β€” ref. to scalar
  • ROOT_DATA β€” header records (TIFF and similar) β€” ref. to flat hash
  • IFD0_DATA β€” primary image TIFF tags β€” ref. to flat hash
  • SUBIFD_DATA β€” Exif private tags β€” ref. to flat hash
  • MAKERNOTE_DATA β€” MakerNote tags (if struct. is known) β€” ref. to flat hash
  • GPS_DATA β€” GPS data of the primary image β€” ref. to flat hash
  • INTEROP_DATA β€” interoperability data β€” ref. to flat hash
  • IFD1_DATA β€” thumbnail-related TIFF tags β€” ref. to flat hash

Setting $what equal to 'ALL' gives you a reference to a hash of hashes, and its top-level hash contains these keys: ROOT_DATA, IFD0_DATA, SUBIFD_DATA, GPS_DATA, INTEROP_DATA, MAKERNOTE_DATA and IFD1_DATA. Each key corresponds to a second-level hash containing a copy of all Exif records present in the IFD (sub)directory corresponding to that key β€” and if that directory isn't present or has no records, the second-level hash still exists, it's just empty. πŸ’€

Note that the Exif record values' format is not checked to be valid according to the Exif standard. Which is, in some sense, consistent with the fact that "unknown" tags are also included in the output. Very much a "we don't gatekeep" situation.

This structure is complicated, so let's just look at an example (and see also the section about valid Exif tags for details on possible records):

    my $hash_ref = $segment->get_Exif_data('ALL', 'TEXTUAL');
                         can give
    $hash_ref = {
           'ROOT_DATA' =>
                { 'Signature'               => [ 42             ],
                  'Endianness'              => [ 'MM'           ],
                  'Identifier'              => [ "Exif\000\000" ],
                  'ThumbnailData'           => [ ... image ...  ], },
           'IFD1_DATA' =>
                { 'ResolutionUnit'          => [ 2              ],
                  'JPEGInterchangeFormatLength' => [ 3922       ],
                  'JPEGInterchangeFormat'   => [ 2204           ],
                  'Orientation'             => [ 1              ],
                  'XResolution'             => [ 72, 1          ],
                  'Compression'             => [ 6              ],
                  'YResolution'             => [ 72, 1          ], },
           'SubIFD_DATA' =>
                { 'ApertureValue'           => [ 35, 10         ],
                  'PixelXDimension'         => [ 2160           ],
                    etc., etc. ....
                  'ExifVersion'             => [ '0210'         ], },
           'MAKERNOTE_DATA' => {},
           'IFD0_DATA' =>
                { 'Model' => [ "KODAK DX3900 ZOOM DIGITAL CAMERA\000" ],
                  'ResolutionUnit'          => [ 2              ],
                    etc., etc. ...
                  'YResolution'             => [ 230, 1         ], },
           'GPS_DATA' => {},
           'INTEROP_DATA' =>
                { 'InteroperabilityVersion' => [ '0100'         ],
                  'InteroperabilityIndex'   => [ "R98\000"      ], }, };

Setting $what equal to '*_DATA' returns a reference to a flat hash, corresponding to one or more IFD (sub)dirs. For instance, 'IMAGE_DATA' is a merge of 'IFD0_DATA' and 'SUBIFD_DATA' β€” and honestly this interface is so much simpler for the end-user, because there's only one dereference level πŸŽ€, and also they don't even need to be aware that the records related to the main image get partitioned into two IFDs. If the (sub)directory isn't present or contains no records, the returned hash exists and is empty. Referencing the previous example:

    my $hash_ref = $segment->get_Exif_data('IMAGE_DATA', 'TEXTUAL');
                         gives
    $hash_ref = {
           'ResolutionUnit'              => [ 2      ],
           'JPEGInterchangeFormatLength' => [ 3922   ],
           'JPEGInterchangeFormat'       => [ 2204   ],
           'Orientation'                 => [ 1      ],
           'XResolution'                 => [ 72, 1  ],
           'Compression'                 => [ 6      ],
           'YResolution'                 => [ 72, 1  ],
           'ApertureValue'               => [ 35, 10 ],
           'PixelXDimension'             => [ 2160   ],
              etc., etc. ....
           'ExifVersion'                 => [ '0210' ], };

Last but not least, setting $what to 'THUMBNAIL' returns a reference to a copy of the actual Exif thumbnail image β€” and no, this one is not included in the set returned by 'THUMB_DATA'. If there's no thumbnail, you get back a reference to the empty string (because the undefined value literally cannot be used here β€” it's assumed to correspond to an error condition). Note that the pointed scalar may be quite large (~ 10^1 KB) πŸ‘€.

If the thumbnail is in JPEG format (which corresponds to the 'Compression' property, in IFD1, set to 6), you can create another JPEG picture object from it, like this:

    my $data_ref = $segment->get_Exif_data('THUMBNAIL');
    my $thumb = new Image::MetaData::JPEG($data_ref);
    print $thumb->get_description();

Now, if you're only interested in reading Exif data in a standard configuration, you can skip the segment-search calls entirely and use JPEG::get_Exif_data directly β€” it's a method of the JPEG class, so all you need is a JPEG structure object. It's basically an interface to the method with the same name in the Segment class, acting on the first Exif APP1 Segment (and if no such segment is present, the undefined value is returned), passing the arguments through. And note that most JPEG files with Exif data contain at most one Exif APP1 segment, so bestie, you are not going to loose anything here. 🌈

A snippet of code for visualising Exif data looks like this:

    while (my ($d, $h) = each %{$image->get_Exif_data('ALL')}) {
      while (my ($t, $a) = each %$h) {
        printf '%-25s\t%-25s\t-> ', $d, $t;
        s/([\000-\037\177-\377])/sprintf '\\%02x',ord($1)/ge,
        $_ = (length $_ > 30) ? (substr($_,0,30) . ' ... ') : $_,
        printf '%-5s', $_ for @$a; print "\n"; } }

πŸ›  How to modify your Exif data πŸ› 

JPEG::Segment::set_Exif_data / JPEG::set_Exif_data

[arguments: "($data, $what, $action)"]

So similarly to the getter case, there's a set_Exif_data method you can call from a picture object, and it does nothing more than look for the first Exif APP1 segment (creating it if there isn't one πŸŽ€) and invoke the method with the same name in the Segment class, passing its arguments through. So the rest of this section is gonna focus on the Segment method. The whole "setting a new thumbnail or erasing it" situation gets dealt with in the last paragraphs of this section. ✨

(Side note from the original author, and I quote the vibe: the APP1 Exif structure is quite complicated, and the number of different possible cases when trying to modify it is very large; therefore, designing a clean and intuitive interface for this task is not trivial. Fell free to suggest improvements and cleaner interfaces.) 🫢

So, Exif records are usually characterised by a numeric key (a tag) β€” we already talked about this in the "getter" section. And since these keys, for valid records, can be translated from numeric to textual form and back, the end user has the freedom to use whichever form better fits their needs. πŸ‘‘

The two forms can even be mixed in the same "setter" call: the method will take care to translate textual tags to numeric tags when possible, and reject the others; then it'll proceed as if all tags were numeric from the very beginning. And records with unknown textual or numeric tags? Those are always rejected. No exceptions, no cap. πŸ’…

πŸŽ€ the arguments to set_Exif_data

okay so set_Exif_data takes three arguments: $data, $what and $action. let's go through them one by one bestie πŸ’…

$data must be a hash reference pointing to a flat hash, and it's holding the key β†’ record value pairs that you, the user, supplied. the "value" half of every hash element can be either:

  • an array reference β€” that's a whole list of values for the record (reminder bestie: some records are multi-valued, they ate and left no crumbs)
  • or a single scalar β€” which gets converted internally into a reference to an array containing only the scalar you handed over

and if a record value is meant to be a null terminated string? you can absolutely pass a plain Perl scalar without that final null character, because it gets inserted automatically for you. slay ✨

$what must be a scalar, and it selects which portion of the Exif APP1 segment your set_Exif_data call is concerned with. so like, obviously, you can only modify one section at a time β€” it's a simplification (for the developer, of course) but also for you, because trying to set all the Exif-like values in one single go would require an offensively complicated data structure just to specify where each record is supposed to land (and note: some records in different sections can have the same numerical tag, so a plain hash would not trivially work 🫠). the valid values for $what are these β€” and heads up, MakerNote data are not currently modifiable:

  • IMAGE_DATA β€” modifies as IFD0_DATA and SUBIFD_DATA; $data type: ref. to flat hash
  • THUMB_DATA β€” this is an alias for IFD1_DATA; $data type: ref. to flat hash
  • THUMBNAIL β€” the actual (un)compressed thumbnail; $data type: ref. to scalar/object
  • ROOT_DATA β€” header records (endianness); $data type: ref. to flat hash
  • IFD0_DATA β€” primary image TIFF tags; $data type: ref. to flat hash
  • SUBIFD_DATA β€” Exif private tags; $data type: ref. to flat hash
  • GPS_DATA β€” GPS data of the primary image; $data type: ref. to flat hash
  • INTEROP_DATA β€” interoperability data in SubIFD; $data type: ref. to flat hash
  • IFD1_DATA β€” thumbnail-related TIFF tags; $data type: ref. to flat hash

the $action argument controls whether the setter adds ($action = 'ADD') records to a given data directory or replaces ($action = 'REPLACE') them. in the first case, each user-supplied record replaces the existing version of that record if it's present, and simply inserts the record if it wasn't already there; however, existing records with no counterpart in your supplied $data hash stay untouched, periodt. in the second case, the record directory is cleared before the user data gets inserted. and note that, since Exif and Exif-like records are non-repeatable in nature, there's no need for an 'UPDATE' action like there is for IPTC (see the IPTC section). πŸ’–

the set_Exif_data routine first checks that the concerned segment is of the appropriate type (Exif APP1), that $data is a hash reference (a scalar reference for the thumbnail), and that $action and $what are valid. if $action is undefined, it defaults to 'REPLACE'. then an appropriate (sub)IFD is created, if absent, and all user-supplied records are checked for consistency (have a look at the appendixes for this). last, records are set in increasing (numerical) tag order, and mandatory data are added, if not present. the return value of the setter routine is always a hash reference; in general it contains records rejected by the specialised routines. if an error occurs in a very early stage of the setter, this reference contains a single entry with key='ERROR' and value set to some meaningful error message. so returning a reference to an empty hash means everything was OK 🫢. an example, concerning the much popular task of changing the DateTime record, follows:

$dt = '1994:07:23 12:14:51';
$hash = $image->set_Exif_data({'DateTime' => $dt}, 'IMAGE_DATA', 'ADD');
print "DateTime record rejected\n" if %$hash;

depending on $what, some of the following notes apply:

ROOT_DATA

the only modifiable item is the 'Endianness' (and it can only be set to big-endian, 'MM', or little-endian, 'II'); everything else is rejected (see the APP1 structure for further details). this only influences how the image is written back to disk (the in-memory representation is always native).

IMAGE_DATA

by specifying this target you can address the IFD0_DATA and SUBIFD_DATA targets at once. first, all records are tried in the IFD0, then the rejected records are tried into SubIFD (and then, they are definitively rejected πŸ’€).

IFD0_DATA

see the canonical, additional and company-assigned tags' sections in the appendixes (this target refers to the primary image). the 'XResolution', 'YResolution', 'ResolutionUnit', and 'YCbCrPositioning' records are forced if not present (to [1,72], [1,72], 2 and 1 respectively). note that the situation would be more complicated if we were dealing with uncompressed (TIFF) primary images.

SUBIFD_DATA

see the private Exif section in the appendixes. the 'ExifVersion', 'ComponentsConfiguration', 'FlashpixVersion', 'ColorSpace', and 'Pixel[XY]Dimension' records are forced if not present (to '0220', '1230', '0100', 1 and 0x0 respectively). image dimensions can be retrieved from the SOF segment with the JPEG structure object's method get_dimensions() and set explicitly by the user if necessary (this cannot be done from within the APP1 segment, because it does not link back to its parent); however, the horizontal field in the SubIFD should not include data padding, while that in the SOF segment does, so the meaning is slightly different and these fields cannot be automatically calculated. πŸ™ˆ

THUMB_DATA (or its alias IFD1_DATA)

see the canonical, additional and company-related tag lists' sections in the appendixes (this target refers to thumbnail properties). the 'XResolution', 'YResolution', 'ResolutionUnit', 'YCbCrSubSampling', 'PhotometricInterpretation' and 'PlanarConfiguration' records are forced if not present (to [1,72], [1,72], 2, [2,1], 2 and 1 respectively). note that some of these records are not necessary for all types of thumbnails, but JPEG readers will probably skip unnecessary information without problems.

GPS_DATA

see the GPS tags section in the appendixes. the 'GPSVersionID' record is forced, if it is not present at the end of the process, because it is mandatory (ver 2.2 is chosen). there are some record inter-correlations which are still neglected here (for instance, the 'GPSAltitude' record can be inserted without providing the corresponding 'GPSAltitudeRef' record).

INTEROP_DATA

JPEG::forge_interoperability_IFD

[arguments: none] see the Interoperability directory section in the appendixes. the 'InteroperabilityIndex' and 'InteroperabilityVersion' records are forced, if they are not present at the end of the process, because they are mandatory ('R98' and ver 1.0 are chosen). note that an Interoperability subIFD should be made as standard as possible: if you just want to add it to the file, it is better to use the forge_interoperability_IFD method, which takes care of all values ('RelatedImageFileFormat' is set to 'Exif JPEG Ver. 2.2', and the dimensions are taken from get_dimensions()).

MAKERNOTE_DATA

see the appendix on MakerNotes for a detailed discussion on how the content of a MakerNote is managed. if there is an error during the parsing of the MakerNote, only those tags which could be fully decoded before the error are returned. note that MakerNote structures are often partially known, so many tags will likely be translated as 'Unknown_tag_...'. MakerNotes cannot be currently modified. 😭

THUMBNAIL

$data must be a reference to a scalar containing the new thumbnail or to a valid Image::MetaData::JPEG object; if it points to an empty string, the thumbnail is erased (the undefined value DOES NOT erase the thumbnail, it generates instead an error 🚨). all thumbnail specific records (see the canonical tags section) are removed, and only those corresponding to the newly inserted thumbnail are calculated and written back. currently, it is not possible to insert an uncompressed thumbnail (this will probably happen in the form of a TIFF image), only JPEG ones are accepted (automatic records contain the type, length and offset). the following code shows how to set and delete a thumbnail.

my $image = new Image::MetaData::JPEG('original_image.jpg');
my $thumb = new Image::MetaData::JPEG('some_thumbnail.jpg');
$image->set_Exif_data($thumb, 'THUMBNAIL');
$image->save('modified_image.jpg');
$image->set_Exif_data(\ '', 'THUMBNAIL');
$image->save('thumbless_image.jpg');

✨ XMP data ("APP1" segments) ✨

XMP (eXtensible Metadata Platform) is a technology, conceived by Adobe Systems, to tag graphic files with metadata, and to manage them during a lifetime made of multiple processing steps. its serialisation (the actual way metadata are saved in the file) is based on RDF (Resource Description Framework) implemented as an application of XML. its flexibility allows to accomodate existing, future and private metadata schemas. in a JPEG file, XMP information is included alongside Exif and IPTC data, and is stored in an APP1 segment on its own starting with the XMP namespace URI and followed by the actual XMP packet (see XMP APP1 segment structure for more details). 🌈

XMP was introduced in 2001 as part of Adobe Acrobat version 5.01. Adobe has a trademark on XMP, and retains control over its specification. source code for the XMP software-development kit was released by Adobe, but with a custom license, whose compatibility with the GNU public license and open-source nature altogether is questioned. πŸ‘€

🎨 Photoshop and IPTC data ("APP13" segments) 🎨

okay bestie, so Adobe's Photoshop program is literally the de-facto standard for image manipulation, and it's been using the APP13 segment since forever to stash non-graphical info like layers, paths, ecc..., plus editorial info modelled on IPTC/NAA recommendations. This module serves up a bunch of methods for managing Photoshop/IPTC data without you having to deal with the low-level representation (though sometimes it makes decisions for you...). The structure of the IPTC data block(s) is managed in detail and separately from the rest, even though this block is kind of a "sub-case" of Photoshop information. The interface is intentionally similar to the one for Exif data. πŸ’…

All public methods have a $what argument that selects which part of the APP13 segment you're working with. Default is 'IPTC'. If $what is invalid, an exception is always raised. Here's the breakdown of what you can access with different $what values (peep the appendices about valid Photoshop-style and IPTC tags for more deets):

  • 'IPTC' or 'IPTC_2' β€” Editorial information like caption, abstract, author, copyright notice, byline, shot site, user defined keywords, and many more; in practise, all what is covered by the IPTC Application Record 2. This is the most common option; the default value of $what, 'IPTC', is a synonym for 'IPTC_2' for backward compatibility (NOT a merge of 'IPTC_1/2').
  • 'IPTC_1' β€” This refers to more obscure pieces of information, contained in the IPTC Envelope Record 1. One is rarely interested by this, exception made for the "Coded Character Set" tag, which is necessary to define a character set different from ASCII (i.e., when you don't write or read in English).
  • 'PHOTOSHOP' or 'PS_8BIM' or 'PS_8BPS' or 'PS_PHUT' β€” Alpha channels, colour information, transfer functions, and many other details concerning the visual rendering of the picture. These fields are most often only modified by an image manipulation program, and not directly by the user. Recent versions of Photoshop (>= 4.0) use a resource data block type equal to '8BIM', and this is the default in this module (so, 'PHOTOSHOP' and 'PS_8BIM' are synonyms). However, some other older or undocumented resource data block types are also allowed.

🌟 JPEG::retrieve_app13_segment 🌟

[arguments: "($index, $what)"]

To work on Photoshop/IPTC data, you first need to select a suitable Photoshop-style APP13 Segment. The retrieve_app13_segment method returns a reference to the $index-th Segment (the first Segment if $index is undefined) which contains information matching the $what argument. If such a Segment does not exist, the method returns the undefined reference. If $index is (-1), the routine returns the number of available suitable APP13 Segments (which is non negative).

Beware, the meaning of $index is influenced by the value of $what. πŸ‘€
my $num_IPTC = $file->retrieve_app13_segment(-1, 'IPTC');
my $ref_IPTC = $file->retrieve_app13_segment($num - 1, 'IPTC');

πŸ’– JPEG::provide_app13_segment πŸ’–

[arguments: "($what)"]

If you want to be sure you have an APP13 Segment suitable for the kind of information you want to write, use the provide_app13_segment method instead. It forces the Segment to be present in the file and returns its reference. If at least one segment matching $what is already present, the first one is returned. Otherwise, the first Photoshop-like APP13 is adapted by inserting an appropriate subdirectory record (update is called automatically). If no such segment exists, it is first created and inserted (the "Photoshop 3.0\000" identifier is used). Note that there is no $index argument here.

my $ref_Photoshop = $file->provide_app13_segment('PHOTOSHOP');

πŸ—‘οΈ JPEG::remove_app13_info πŸ—‘οΈ

[arguments: "($index, $what)"]

If you want to remove all traces of some flavour of APP13 information from the $index-th APP13 Photoshop-style Segment, use the remove_app13_info method with $what set to the appropriate value. If, after this, the segment is empty, it is eliminated from the list of segments in the file. If $index is (-1), all APP13 Segments are affected at once.

Beware, the meaning of $index is influenced by the value of $what. 😭
$file->remove_app13_info(3, 'PHOTOSHOP');
$file->remove_app13_info(-1, 'IPTC');
$file->remove_app13_info(0, 'IPTC_1');

πŸ” How to inspect and modify your IPTC data πŸ”

✨ JPEG::Segment::get_app13_data ✨

[arguments: "($type, $what)"]

Once you have a Segment reference pointing to your favourite IPTC-enabled APP13 Segment, you might want to peek at the records it contains. Use the get_app13_data method for this: its behaviour is controlled by the $type and $what argument (here, $what is 'IPTC_1' or 'IPTC_2' alias 'IPTC', of course). It returns a reference to a hash containing a copy of the list of the appropriate IPTC records, if present, undef otherwise: each element of the hash is a pair (key, arrayref), where arrayref points to an array with the real values (some IPTC records are repeatable so multiple values are possible). The record keys can be the native numeric keys ($type eq 'NUMERIC') or translated textual keys ($type eq 'TEXTUAL', default); in any case, the record values are untranslated. If a numeric key stored in the JPEG file is unknown, and a textual translation is requested, the name of the key becomes "Unknown_tag_$tag".

Note that there is no check on the validity of IPTC records' values: their format is not checked and one or multiple values can be attached to a single tag independently of its repeatability. This is, in some sense, consistent with the fact that also "unknown" tags are included in the output. If $type or $what is invalid, an exception is thrown out.

Here's an example of how to extract and display IPTC data:

my $hash_ref = $segment->get_app13_data('TEXTUAL', 'IPTC');
while (my ($key, $vals) = each %$hash_ref) {
   printf "# %20s =", $key; print " '$_'" for @$vals; print "\n"; }

### This could print:
# DateCreated         = '19890207'
# ByLine              = 'Interesting picture' 'really'
# Category            = 'POL'
# Keywords            = 'key-1' 'key-2' 'key-99'
# OriginatingProgram  = 'Mapivi'

πŸ› οΈ JPEG::Segment::set_app13_data πŸ› οΈ

[arguments: "($data, $action, $what)"]

The hash returned by get_app13_data can be edited and reinserted with the set_app13_data method, whose arguments are $data, $action and, as usual, $what. If $action or $what is invalid, an exception is generated. This method accepts IPTC data in various formats and updates the corresponding subdirectory in the segment. The key type of each entry in the input hash can be numeric or textual, independently of the others (the same key can appear in both forms, the corresponding values will be put together). The value of each entry can be an array reference or a scalar (you can use this as a shortcut for value arrays with only one value). The $action argument can be:

  • ADD β€” new records are added and nothing is deleted; however, if you try to add a non-repeatable record which is already present, the newly supplied value ejects (replaces) the pre-existing value.
  • UPDATE β€” new records replace those characterised by the same tags, but the others are preserved. This makes it possible to modify some repeatable IPTC records without deleting the other tags.
  • REPLACE β€” all records present in the IPTC subdirectory are deleted before inserting the new ones (this is the default action).

If, after implementing the changes required by $action, any mandatory dataset (according to the IPTC standard), is still undefined, it is added automatically. This often concerns version datasets, with numeric index 0.

The return value is a reference to a hash containing the rejected key-values entries. The entries of %$data are not modified. An entry in the %$data hash can be rejected for various reasons (you might want to have a look at appendix about valid IPTC tags for further information):

  • a) the tag is undefined or not known;
  • b) the entry value is undefined or points to an empty array;
  • c) the non-repeatability constraint is violated;
  • d) the tag is marked as invalid;
  • e) a value is undefined
  • f) the length of a value is invalid;
  • g) a value does not match its mandatory regular expression.
$segment->set_app13_data($additional_data, 'ADD', 'IPTC');

A snippet of code for changing IPTC data looks like this:

my $segment = $file->provide_app13_segment('IPTC');
my $hashref_1 = { CodedCharacterSet => "\033\045G" }; # UTF-8
my $hashref_2 = { ObjectName => 'prova',
                  ByLine     => 'ciao',
                  Keywords   => [ 'donald', 'duck' ],
                  SupplementalCategory => ['arte', 'scienza', 'diporto'] };
$segment->set_app13_data($hashref_2, 'REPLACE', 'IPTC');
$segment->provide_app13_subdir('IPTC_1');
$segment->set_app13_data($hashref_1, 'ADD', 'IPTC_1');

πŸ“– JPEG::get_app13_data πŸ“–

[arguments: "($type, $what)"]

If you're only interested in reading IPTC data in a standard configuration, you can skip most of the previous calls and use JPEG::get_app13_data directly (it's a method in the JPEG class, so you only need a JPEG structure object). This is an interface to the method with the same name in the Segment class, acting on the first relevant APP13 Segment (if no such segment is present, the undefined value is returned) and passing the arguments through. Note that most JPEG files with Photoshop/IPTC data contain at most one APP13 segment, so you are not going to "loose" anything here.

A snippet of code for visualising IPTC data looks like this:

my $hashref = $file->get_app13_data('TEXTUAL', 'IPTC');
while (my ($tag, $val_arrayref) = each %$hashref) {
    printf '%25s --> ', $tag;
    print "$_ " for @$val_arrayref; print "\n"; }

πŸ’… JPEG::set_app13_data

[arguments: ($data, $action, $what)] Okay so obviously there's a matching JPEG::set_app13_data method, and she's the write twin β€” she pushes data onto the JPEG object without you ever having to think about Segments, bestie. Not even a little. She just grabs the first available suitable Segment; and if that's not doable, she makes a brand new Segment and initialises it herself. Why? Because internally she calls JPEG::provide_app13_segment, and not JPEG::retrieve_app13_segment like JPEG::get_app13_data does. Different girl, different job. ✨

$file->set_app13_data($hashref, 'UPDATE', 'IPTC');

🎨 How to inspect and modify your Photoshop data

Inspecting and modifying your Photoshop data β€” meaning the non-IPTC stuff living inside a Photoshop-style APP13 segment β€” works pretty much the same way as the IPTC flow, except you set $what to 'PHOTOSHOP' (alias 'PS_8BIM'), or to the rarely-spotted 'PS_8BPS' and 'PS_PHUT'. I'm not gonna retype the whole thing here because that would be so much, so go read the IPTC section, fr fr β€” this section is only here to flag the differences. And if you don't know what an APP13 segment looks like or what "resource data block" means, go peek at the Photoshop-style tags' section first. 🫢

On the subject of get_app13_data: the one thing worth saying is that resource block names get appended onto the list of values for each tag β€” even when they're undefined β€” so the list length always comes out even. Iconic behaviour, honestly.

set_app13_data is where it gets messier: non-IPTC Photoshop specs are way less uniform than IPTC ones, and actually checking whether user-supplied data is correct would be an enumerative task (i.e. a whole thing). Right now this module does not run any syntax check on non-IPTC data β€” though that could totally change in the future (any contribution is welcome, no cap πŸ‘€). The only thing that is checked is whether tags (or, as they're called in this context, "resource block identifiers") are in the allowed tags list β€” see the Photoshop-style tags' table for the deets. And the IPTC/NAA tag? Rejected, obviously. IPTC data has to go in with $what set to 'IPTC' or its siblings, periodt.

Even though nobody says it out loud, it seems that non-IPTC Photoshop tags are non-repeatable (let me know if that's not the case, bestie), so two resource blocks with the same tag shouldn't be a thing. Because of that, the 'UPDATE' action gets internally switched to 'ADD'. And since the resource block structure isn't explored at all, every resource block gets treated as single-valued with a value type of $UNDEF.

So here's the tea for your user-supplied data hash: if a tag key returns a data array reference, only the first element β€” which cannot be undefined β€” gets used as the resource block value. If a second element is there, it becomes the resource block name (otherwise that name is set to the null string). Supplying more than two elements is an error and the record gets rejected. Simple as. πŸ’…

my $segment = $file->provide_app13_segment('PHOTOSHOP');
my $hashref = {
    GlobalAngle    => pack('N', 0x1e),
    GlobalAltitude => pack('N', 0x1e),
    CopyrightFlag  => "\001",
    IDsBaseValue   => [ pack('N', 1), 'Layer ID Generator Base' ] };
$segment->set_app13_data($hashref, 'ADD', 'PHOTOSHOP');

πŸ“ NOTES

πŸ—“οΈ On the subject of year specification in a date

Right now there are eight fields whose whole purpose is storing a date in a JPEG picture, and they are:

  • 'DateTime', 'DateTimeOriginal' and 'DateTimeDigitized' (in IFD0/1 or SubIFD)
  • 'GPSDateStamp' (in the GPS section)
  • 'ReleaseDate', 'ExpirationDate', 'DateCreated' and 'DigitalCreationDate' (in the IPTC section)

Most of these dates are about some electronic treatment of images β€” a kind of process that literally did not exist before the late twentieth century. Two of them refer to release and expiration dates in the IPTC standard, so they shouldn't be set to a date before the standard itself was introduced. But listen: there are users who want to use some of these fields in a non-conventional way, to refer to dates when analog photography was a thing but digital photography was not. So, for that reason, all tags (but one) can be written with a year starting from 1800 β€” and not from 1900 like in earlier releases. That said, users are advised to check the "specifications" for these tags before setting the date, and to take responsibility for their own non-conventionality. You've been warned, babe. πŸ™ˆ

There's one notable exception to all of the above: the IPTC 'DateCreated' dataset, which should explicitly refer to the creation date of the object represented in the picture β€” and that could be many centuries in the past. For this dataset a special regular expression is provided that allows a date in the full ISO-8601 YYYY-MM-DD format. (Tho ngl, even ISO-8601 doesn't allow a date before 0AD, so not every masterwork from ancient Greece can be tagged this way... let me know if I'm wrong!) I'm, of course, still open to suggestions and reconsiderations on this whole subject. πŸ›οΈ

πŸ”§ On the problem of MakerNote corruption and ways to overcome it

A widespread problem with Exif maker notes is that there is no common standard for how to parse and rewrite the information in the MakerNote data area. That's why most programs dealing with Exif JPEG files corrupt the MakerNote on saving, or just decide to drop it altogether. And be aware β€” there existed programs known to hang when they try to read a corrupt maker note. πŸ’€

In fact, many maker notes contain a non-standard IFD structure, with some tags storing file offsets (go see the documentation page describing the IFD structure). So saving a maker note without adjusting the internal offsets basically reduces the note to garbage. Re-dumping a maker note after changing the Exif APP1 segment endianness incurs the exact same problem, because no internal byte-swap is performed. Rough.

A few countermeasures have been introduced in this package to try to cure some maker note problems. The first one concerns the correct byte order β€” the endianness, which isn't always the same as the one used in the Exif segment β€” and which therefore needs not be known in advance. It's actually determined by using the fact that, if the note is IFD-like (even non-standard), the number of tags is always in the range [1,255], so the two-byte tag count always has its most significant byte set to zero and its least significant byte set to non-zero. Clever girl. ✨

There's also a prediction and correction mechanism for the offsets in the interoperability arrays, based on the simple assumption that the absolute value of offsets can be wrong, but their differences are always right. So if you can get the first one right... a good bet is the address of the byte immediately following the next_IFD link (or the tag list, if that link is absent). If the parsing process doesn't end successfully, this mechanism gets enabled, and its "corrected" findings are stored instead of the original ones β€” but only if it manages to cure the problems, i.e. if the second try at parsing the note succeeds. πŸ‘‘


πŸ“Š CURRENT STATUS

A lot of other routines for modifying other meta-data could be added in the future. Here's the current status of various meta-data Segments (only APP and COM Segments, btw):

  • COM β€” possible content: User comments β€” status: parse/read/write
  • APP0 β€” possible content: JFIF data (+ thumbnail) β€” status: parse/read
  • APP1 β€” possible content: Exif or XMP data β€” status: parse/read[Exif]/write[Exif]
  • APP1 β€” possible content: Maker notes β€” status: parse/read
  • APP2 β€” possible content: FPXR data or ICC profiles β€” status: parse
  • APP3 β€” possible content: additional Exif-like data β€” status: parse
  • APP4 β€” possible content: HPSC β€” status: nothing
  • APP12 β€” possible content: PreExif ASCII meta β€” status: parse
  • APP13 β€” possible content: IPTC and PhotoShop data β€” status: parse/read/write
  • APP14 β€” possible content: Adobe tags β€” status: parse

πŸ’€ KNOWN BUGS

USE WITH CAUTION! THIS IS EXPERIMENTAL SOFTWARE!

This module is still experimental and not yet finished. In particular, it is far from being well tested, and some interfaces could change depending on user feedback. The ability to modify maker notes is not yet implemented β€” and while we're on it, go have a look at the MakerNote appendix for a general note on the problem of MakerNote corruption. APP13 data spanning multiple Segments is not correctly read/written. And most APP12 Segments don't fit the structure parsed by parse_app12() β€” probably there's some standard out there that I simply don't know about. 😭

✨ OTHER PACKAGES ✨

okay so bestie, there are other packages out there in the free software arena, and their feature set overlaps a LOT with what this one does. here's a probably-incomplete list 🫢 but real talk: none of them is (or was) completely satisfactory with respect to this package's objectives, which are:

  • being a single package dealing with all types of meta-information in read/write mode in a JPEG (and possibly TIFF) file
  • depending on the least possible number of non standard packages and/or external programs or libraries
  • being open-source and written in Perl

and of course, most of these objectives are far from being reached .... 😭


  • Image::ExifTool by Phil Harvey β€” ExifTool is a Perl module with an included command-line application for reading and writing meta information in image files. It recognizes EXIF, GPS, IPTC, XMP, JFIF, GeoTIFF, ICC Profile, Photoshop IRB and ID3 meta information as well as the maker notes of many digital cameras including Canon, Casio, FujiFilm, Kodak, Leaf, Minolta/Konica-Minolta, Nikon, Olympus/Epson, Panasonic/Leica, Pentax/Asahi, Ricoh, Sanyo and Sigma/Foveon. It was started as a highly customisable, read-only report tool, capable of organising the results in various ways. Since version 4.10 (beginning of 2005) it added the ability to modify and rewrite JPEG tags. So sad there are now two projects with such a large overlap. πŸ’€
  • Image::IPTCInfo by Josh Carter β€” this is a CPAN module for extracting IPTC image meta-data. It allows reading IPTC data (there is an XML and also an HTML output feature) and manipulating them through native Perl structures. This library does not implement a full parsing of the JPEG file, so I did not consider it as a good base for the development of a full-featured module. Moreover, I don't like the separate treatment of keywords and supplemental categories. πŸ™ˆ
  • JPEG::JFIF by Marcin Krzyzanowski, Image::EXIF by Sergey Prozhogin and exiftags by Eric M. Johnston β€” JPEG::JFIF is a very small CPAN module for reading meta-data in JFIF/JPEG format files. In practice, it only recognises a subset of the IPTC tags in APP13, and the parsing code is not suitable for being reused for a generic JPEG segment. Image::Exif is just a Perl wrapper around exiftags, which is a program parsing the APP1 section in JPEG files for Exif meta-data (it supports a variety of MakerNotes). exiftags can also rewrite comments and date and time tags.
  • Image::Info by Gisle Aas β€” this CPAN module extracts meta information from a variety of graphic formats (including JPEG and TIFF). So, it is not specifically about JPEG segments: reported information includes media type, extension, width, height, colour type, comments, Interlace, Compression, Gamma, and LastModificationTime. For JPEG files, it additionally reports from JFIF (APP0) and Exif (APP1) segments (including MakerNotes). This module does not allow for editing. 🚫
  • exif by Martin Krzywinski and exifdump.py by Thierry Bousch β€” these are two basic scripts to extract Exif information from JPEGs. The first script is written in Perl and targets Canon pictures. The second one is written in Python, and it only works on JPEG files beginning with an APP1 section after the SOI. So, they are much simpler than all other programs/libraries described here. Of course, they cannot modify Exif data.
  • jhead by Matthias Wandel β€” the jhead program (written in C) is used to display JPEG comments and Exif data, and to perform limited manipulation of Exif headers (such as changing the internal time-stamps, removing the thumbnail, or transferring headers back into edited images) and comments. Exif header data modification is very limited, as jhead's internal implementation of the file system contained in the Exif header is read-only; there, for instance, no way to replace the thumbnail in the Exif header with another. πŸ₯²
  • exifprobe by Duane H. Hesser β€” this is a C program which examines and reports the contents and structure of JPEG and TIFF image files. It recognises all standard JPEG markers and reports the contents of any properly structured TIFF IFD encountered, even when entry tags are not recognised. Camera MakerNotes are included. GPS and GeoTIFF tags are recognised and entries printed in "raw" form, but are not expanded. The output is nicely formatted, with indentation and colouration; this program is a great tool for inspecting a JPEG/TIFF structure while debugging. πŸ”₯
  • libexif by Lutz MΓΌller β€” this is a library, written in C, for parsing, editing, and saving Exif data. All Exif tags described in Exif standard 2.1 are supported. Libexif can only handle some maker notes, and even those not very well. It is used by a number of front-ends, including: Exif (read-only command-line utility), gexif (a GTK+ front-end for editing Exif data), gphoto2 (command-line front-end to libgphoto2, a library to access digital cameras), gtkam (a GTK+ front-end to libgphoto2), thirdeye (a digital photos organiser and driver for eComStation).
  • jpegrdf by Norman Walsh β€” this is a Java application for manipulating (read/write) RDF meta-data in the comment sections of JPEG images (is this the same thing which can be found in APP1 segments in XMP format?). It can also access and convert into RDF the Exif tags and a few other general properties. However, I don't want to rely on a Java environment being installed in order to be able to access these properties. πŸ™…
  • OpenExif by Eastman Kodak Company β€” this is an object-oriented interface written in C++ to Exif formatted JPEG image files. It is very complete and sponsored by a large company, so it is to be considered a sort of reference. The toolkit allows creating, reading, and modifying the meta-data in the Exif file. It also provides means of getting and setting the main image and the thumbnail image. OpenExif is also extensible, and Application segments can be added. πŸ‘‘

🌸 AUTHOR 🌸

Stefano Bettelli, bettelli@cpan.org


Copyright (C) 2004,2005,2006 by Stefano Bettelli

This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License. See the COPYING and LICENSE file for the license terms.


πŸ¦‹ SEE ALSO πŸ¦‹

Have a look at the technical appendixes of the Image::MetaData::JPEG module [M in the following], packaged as separate documents: they contain a description of segment structures [M::Structures], and lists of valid tags [M::TagLists], including a tentative description of some MakerNote formats [M::MakerNotes]. See also your current perl(1) documentation, an explanation for the General Public License and the manual pages of the following optional Perl modules: Image::ExifTool(3pm), Image::IPTCInfo(3pm), JPEG::JFIF(3pm), Image::EXIF(3pm) and Image::Info(3pm).


and that's a wrap on the whole page bestie, you read all of it, iconic behavior fr fr πŸŽ€ this doc came from perl v5.34.0 and got stamped on 2022-03-07 β€” go forth and read your JPEGs πŸ’…βœ¨