Skip to content

Commit d084cf7

Browse files
committed
Added EXIF output support for JPEG, WebP and AVIF format writers.
Reading of EXIF data is, however, only currently supported by the JPEG input reader, so no TIFF EXIF input support for the moment.
1 parent 20644e1 commit d084cf7

11 files changed

+149
-14
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
28/10/2024:
2+
- Added EXIF output support for JPEG, WebP and AVIF format writers. Reading of EXIF data is, however, only
3+
currently supported by the JPEG input reader, so no TIFF EXIF input support for the moment.
4+
5+
16
27/10/2024:
27
- Added ability to inject ICC profile and XMP metadata into pre-encoded tiles returned from TIFF images. These
38
are essentially raw bitstreams with no container. Works for both JPEG and WebP formats.

src/AVIFCompressor.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ unsigned int AVIFCompressor::Compress( RawTile& rawtile ){
164164
// Add ICC profile and XMP metadata to our image
165165
writeICCProfile();
166166
writeXMPMetadata();
167+
writeExifMetadata();
167168

168169

169170
if( (OK=avifEncoderAddImage( encoder, avif, 1, AVIF_ADD_IMAGE_FLAG_SINGLE )) != AVIF_RESULT_OK ){
@@ -238,3 +239,21 @@ void AVIFCompressor::writeXMPMetadata()
238239
}
239240
#endif
240241
}
242+
243+
244+
245+
/// Write EXIF metadata
246+
void AVIFCompressor::writeExifMetadata()
247+
{
248+
size_t len = exif.size();
249+
if( len == 0 ) return;
250+
251+
#if AVIF_VERSION_MAJOR < 1
252+
// No return from version < 1
253+
avifImageSetMetadataExif( avif, (const uint8_t*) exif.c_str(), len );
254+
#else
255+
if( avifImageSetMetadataExif( avif, (const uint8_t*) exif.c_str(), len ) != AVIF_RESULT_OK ){
256+
throw string( "AVIFCompressor :: Error adding EXIF metadata" );
257+
}
258+
#endif
259+
}

src/AVIFCompressor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class AVIFCompressor : public Compressor {
5252
/// Write XMP metadata
5353
void writeXMPMetadata();
5454

55+
/// Write EXIF metadata
56+
void writeExifMetadata();
57+
5558

5659
public:
5760

src/CVT.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,15 @@ void CVT::send( Session* session ){
536536
compressor->embedXMPMetadata( true );
537537
}
538538

539+
// Always embed EXIF metadata in CVT function
540+
if( (*session->image)->getMetadata("exif").size() > 0 ){
541+
if( session->loglevel >= 3 ){
542+
*(session->logfile) << "CVT :: Embedding EXIF metadata with size "
543+
<< (*session->image)->getMetadata("exif").size() << " bytes" << endl;
544+
}
545+
compressor->embedExifMetadata( true );
546+
}
547+
539548

540549
// Initialise our output compression object
541550
compressor->InitCompression( complete_image, resampled_height );

src/Compressor.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class Compressor {
6262
bool embedXMP;
6363
std::string xmp;
6464

65+
// EXIF metadata
66+
bool embedEXIF;
67+
std::string exif;
68+
6569
/// Write metadata
6670
virtual void writeMetadata() {};
6771

@@ -74,6 +78,9 @@ class Compressor {
7478
/// Write XMP metadata
7579
virtual void writeXMPMetadata() {};
7680

81+
/// Write EXIF metadata
82+
virtual void writeExifMetadata() {};
83+
7784

7885
public:
7986

@@ -88,7 +95,8 @@ class Compressor {
8895
dpi_y( 0 ),
8996
dpi_units( 0 ),
9097
embedICC( false ),
91-
embedXMP( false ) {};
98+
embedXMP( false ),
99+
embedEXIF( false ) {};
92100

93101

94102
virtual ~Compressor() {};
@@ -125,6 +133,9 @@ class Compressor {
125133
/** @param embed Whether XMP metadata should be embedded */
126134
inline void embedXMPMetadata( const bool embed ){ this->embedXMP = embed; }
127135

136+
/// Embed EXIF metadata
137+
/** @param embed Whether EXIF metadata should be embedded */
138+
inline void embedExifMetadata( const bool embed ){ this->embedEXIF = embed; }
128139

129140
/// Set general metadata
130141
/** @param metadata Metadata list */
@@ -146,9 +157,21 @@ class Compressor {
146157
xmp = it->second;
147158
this->metadata.erase( it );
148159
}
160+
161+
// Extract EXIF chunk if it exists and remove from list
162+
it = this->metadata.find("exif");
163+
if( it != this->metadata.end() ){
164+
exif = it->second;
165+
this->metadata.erase( it );
166+
}
167+
149168
};
150169

151170

171+
/// Set quality level
172+
/** @param quality quality level */
173+
virtual void setQuality( int quality ) {};
174+
152175

153176
/// Initialise strip based compression
154177
/** If we are doing a strip based encoding, we need to first initialise

src/JPEGCompressor.cc

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ inline double round(double r) { return (r > 0.0) ? floor(r + 0.5) : ceil(r - 0.5
5555
#define XMP_PREFIX "http://ns.adobe.com/xap/1.0/%c%s"
5656
#define XMP_PREFIX_SIZE 29
5757

58+
// EXIF definitions
59+
#define EXIF_PREFIX "Exif\0\0"
60+
#define EXIF_PREFIX_SIZE 6
5861

5962

6063
/* IIPImage version of the JPEG error_exit function. We want to pass control back
@@ -82,7 +85,7 @@ extern "C" {
8285
* way of doing this, but this seems to work :/
8386
*/
8487
void setup_error_functions( jpeg_compress_struct *a ){
85-
a->err->error_exit = iip_error_exit;
88+
a->err->error_exit = iip_error_exit;
8689
}
8790
}
8891

@@ -185,7 +188,8 @@ void JPEGCompressor::InitCompression( const RawTile& rawtile, unsigned int strip
185188
// Calculate our metadata storage requirements
186189
unsigned int metadata_size =
187190
(icc.size()>0 ? (icc.size()+ICC_OVERHEAD_LEN) : 0) +
188-
(xmp.size()>0 ? (xmp.size()+XMP_PREFIX_SIZE) : 0);
191+
(xmp.size()>0 ? (xmp.size()+XMP_PREFIX_SIZE) : 0) +
192+
(exif.size()>0 ? (exif.size()+EXIF_PREFIX_SIZE) : 0);
189193

190194
// Allocate enough memory for our header and metadata
191195
unsigned long output_size = metadata_size + MX;
@@ -225,7 +229,10 @@ void JPEGCompressor::InitCompression( const RawTile& rawtile, unsigned int strip
225229

226230
// Add XMP metadata
227231
writeXMPMetadata();
228-
232+
233+
// Add EXIF metadata
234+
writeExifMetadata();
235+
229236
// Copy the encoded JPEG header data to a separate buffer
230237
size_t datacount = dest->source_size - dest->pub.free_in_buffer;
231238
header_size = datacount;
@@ -337,7 +344,8 @@ unsigned int JPEGCompressor::Compress( RawTile& rawtile )
337344
// Calculate our metadata storage requirements
338345
unsigned int metadata_size =
339346
(icc.size()>0 ? (icc.size()+ICC_OVERHEAD_LEN) : 0) +
340-
(xmp.size()>0 ? (xmp.size()+XMP_PREFIX_SIZE) : 0);
347+
(xmp.size()>0 ? (xmp.size()+XMP_PREFIX_SIZE) : 0) +
348+
(exif.size()>0 ? (exif.size()+EXIF_PREFIX_SIZE) : 0);
341349

342350
// Allocate enough memory for our compressed output data
343351
// - compressed images at overly high quality factors can be larger than raw data
@@ -373,6 +381,9 @@ unsigned int JPEGCompressor::Compress( RawTile& rawtile )
373381
// Add XMP metadata
374382
writeXMPMetadata();
375383

384+
// Add EXIF metadata
385+
writeExifMetadata();
386+
376387

377388
// Compress the image line by line
378389
JSAMPROW row[1];
@@ -516,9 +527,27 @@ void JPEGCompressor::writeXMPMetadata()
516527

517528

518529

530+
void JPEGCompressor::writeExifMetadata()
531+
{
532+
// Skip if EXIF embedding is disabled or no EXIF present
533+
if( !embedEXIF || exif.empty() ) return;
534+
535+
size_t len = exif.size();
536+
537+
// Need to add prefix (need do this with memcpy because of null bytes)
538+
char exifstr[65536];
539+
memcpy( exifstr, EXIF_PREFIX, EXIF_PREFIX_SIZE );
540+
memcpy( exifstr+EXIF_PREFIX_SIZE, exif.c_str(), len );
541+
542+
// Write out our marker
543+
jpeg_write_marker( &cinfo, JPEG_APP0+1, (const JOCTET*) exifstr, len+EXIF_PREFIX_SIZE );
544+
}
545+
546+
547+
519548
void JPEGCompressor::injectMetadata( RawTile& rawtile )
520549
{
521-
if( (!embedICC && !embedXMP) || (icc.empty() && xmp.empty()) ) return;
550+
if( (!embedICC && !embedXMP &&!embedEXIF) || (icc.empty() && xmp.empty() && exif.empty()) ) return;
522551

523552
// Initialize our compression structure
524553
InitCompression( rawtile, 0 );

src/JPEGCompressor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class JPEGCompressor: public Compressor{
7878
/// Write XMP metadata
7979
void writeXMPMetadata();
8080

81+
/// Write EXIF metadata
82+
void writeExifMetadata();
83+
8184

8285
public:
8386

src/PNGCompressor.cc

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ void PNGCompressor::InitCompression( const RawTile& rawtile, unsigned int strip_
105105
// Calculate our metadata storage requirements
106106
unsigned int metadata_size =
107107
(icc.size()>0 ? (icc.size()+ICC_OVERHEAD_SIZE) : 0) +
108-
(xmp.size()>0 ? (xmp.size()+XMP_OVERHEAD_SIZE) : 0);
108+
(xmp.size()>0 ? (xmp.size()+XMP_OVERHEAD_SIZE) : 0) +
109+
(exif.size()>0 ? exif.size() : 0);
109110

110111
// Allocate enough memory for our header and metadata
111112
unsigned long output_size = metadata_size + MX;
@@ -243,7 +244,8 @@ unsigned int PNGCompressor::Compress( RawTile& rawtile )
243244
// Calculate our metadata storage requirements
244245
unsigned int metadata_size =
245246
(icc.size()>0 ? (icc.size()+ICC_OVERHEAD_SIZE) : 0) +
246-
(xmp.size()>0 ? (xmp.size()+XMP_OVERHEAD_SIZE) : 0);
247+
(xmp.size()>0 ? (xmp.size()+XMP_OVERHEAD_SIZE) : 0) +
248+
(exif.size()>0) ? exif.size() : 0;
247249

248250
// Allocate enough memory for our compressed output data - make sure there is extra buffering
249251
// Note that compressed images at overly high quality factors can be larger than raw data
@@ -412,6 +414,9 @@ void PNGCompressor::writeMetadata()
412414

413415
// Write XMP chunk
414416
writeXMPMetadata();
417+
418+
// Write EXIF chunk
419+
writeExifMetadata();
415420
}
416421

417422

@@ -474,3 +479,17 @@ void PNGCompressor::writeXMPMetadata()
474479
png_set_text( dest.png_ptr, dest.info_ptr, &text, 1 );
475480

476481
}
482+
483+
484+
485+
void PNGCompressor::writeExifMetadata()
486+
{
487+
// Skip if EXIF embedding is disabled or no EXIF present
488+
if( !embedEXIF || exif.empty() ) return;
489+
490+
#ifdef PNG_eXIf_SUPPORTED
491+
// Write out EXIF chunk
492+
png_set_eXIf_1( dest.png_ptr, dest.info_ptr, exif.size(), (png_bytep) exif.c_str() );
493+
#endif
494+
495+
}

src/PNGCompressor.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ class PNGCompressor : public Compressor {
8080
/// Write XMP metadata
8181
void writeXMPMetadata();
8282

83-
83+
/// Write EXIF metadata
84+
void writeExifMetadata();
85+
86+
8487
public:
8588

8689
/// Constructor

src/WebPCompressor.cc

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,13 @@ unsigned int WebPCompressor::Compress( RawTile& rawtile ){
126126
WebPData output;
127127

128128
// Use the WebP muxer only if we need to
129-
if( icc.size() > 0 || xmp.size() > 0 ){
129+
if( icc.size() > 0 || xmp.size() > 0 || exif.size() > 0 ){
130130

131131
// Add ICC profile and XMP metadata to our output bitstream
132132
writeICCProfile();
133133
writeXMPMetadata();
134-
134+
writeExifMetadata();
135+
135136
// Add our image data chunk
136137
WebPData chunk;
137138
chunk.bytes = writer.mem;
@@ -213,15 +214,32 @@ void WebPCompressor::writeXMPMetadata()
213214

214215

215216

217+
/// Write EXIF metadata
218+
void WebPCompressor::writeExifMetadata()
219+
{
220+
// Skip if EXIF embedding disabled or no EXIF chunk exists
221+
if( !embedEXIF || exif.empty() ) return;
222+
223+
WebPData chunk;
224+
chunk.bytes = (const uint8_t*) exif.c_str();
225+
chunk.size = exif.size();
226+
227+
if( WebPMuxSetChunk( mux, "EXIF", &chunk, 0 ) != WEBP_MUX_OK ){
228+
throw string( "WebPCompressor :: Error setting EXIF chunk" );
229+
}
230+
}
231+
232+
233+
216234
void WebPCompressor::injectMetadata( RawTile& rawtile )
217235
{
218-
if( (!embedICC && !embedXMP) || (icc.empty() && xmp.empty()) ) return;
236+
if( (!embedICC && !embedXMP && !embedEXIF) || (icc.empty() && xmp.empty() && exif.empty()) ) return;
219237

220238
WebPData input;
221239
input.bytes = (const uint8_t*) rawtile.data;
222240
input.size = rawtile.dataLength;
223241

224-
// Only add ICC or metadata if we have a raw WebP stream
242+
// Only add ICC or metadata if we have a raw WebP stream:
225243
// Bytes 8-16 should be exactly "WEBPVP8 " (lossy) or "WEBPVP8L" (lossless)
226244
static const unsigned char lossy_header[8] = {0x57,0x45,0x42,0x50,0x56,0x50,0x38,0x20};
227245
static const unsigned char lossless_header[8] = {0x57,0x45,0x42,0x50,0x56,0x50,0x38,0x4c};
@@ -231,9 +249,10 @@ void WebPCompressor::injectMetadata( RawTile& rawtile )
231249

232250
WebPData output;
233251

234-
// Add ICC profile and XMP metadata to our output bitstream
252+
// Add ICC profile, XMP and EXIF metadata to our output bitstream
235253
writeICCProfile();
236254
writeXMPMetadata();
255+
writeExifMetadata();
237256

238257
// Add our raw image bitstream data
239258
if( WebPMuxSetImage( mux, &input, 0 ) != WEBP_MUX_OK ){

0 commit comments

Comments
 (0)