Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d18bc30f authored by Dichen Zhang's avatar Dichen Zhang
Browse files

Update EXIF

Modify the EXIF package if it exists, or create a single-entry EXIF
package (API-1).
Extract / modify the first EXIF package (if it exists) or create a
single-entry EXIF package from the primary image (API-2, API-3).
Change the order of the EXIF and XMP package, new order:

SOI
EXIF
XMP
[other packages if they appears in the primary image]
primary image without EXIF
secondary image

Test: recoverymap_test and check the encoded file with a hex editor
Bug: b/252835416
Change-Id: I3d8c4a3fe3cff63f8b57277511aa801b9dd1b75d
parent 15e72193
Loading
Loading
Loading
Loading
+31 −6
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ public:
     */
    void* getDecompressedImagePtr();
    /*
     * Returns the decompressed raw image buffer size. This method must be called only after
     * Returns the decompressed raw image buffer size. This mgit ethod must be called only after
     * calling decompressImage().
     */
    size_t getDecompressedImageSize();
@@ -67,14 +67,35 @@ public:
    void* getXMPPtr();
    /*
     * Returns the decompressed XMP buffer size. This method must be called only after
     * calling decompressImage().
     * calling decompressImage() or getCompressedImageParameters().
     */
    size_t getXMPSize();

    /*
     * Returns the EXIF data from the image.
     */
    void* getEXIFPtr();
    /*
     * Returns the decompressed EXIF buffer size. This method must be called only after
     * calling decompressImage() or getCompressedImageParameters().
     */
    size_t getEXIFSize();
    /*
     * Returns the position offset of EXIF package
     * (4 bypes offset to FF sign, the byte after FF E1 XX XX <this byte>),
     * or -1  if no EXIF exists.
     */
    int getEXIFPos() { return mExifPos; }
    /*
     * Decompresses metadata of the image.
     */
    bool getCompressedImageParameters(const void* image, int length,
                                      size_t* pWidth, size_t* pHeight,
                                      std::vector<uint8_t>* &iccData,
                                      std::vector<uint8_t>* &exifData);
    /*
     * Extracts EXIF package and updates the EXIF position / length without decoding the image.
     */
    bool extractEXIF(const void* image, int length);

private:
    bool decode(const void* image, int length);
@@ -89,10 +110,14 @@ private:
    std::vector<JOCTET> mResultBuffer;
    // The buffer that holds XMP Data.
    std::vector<JOCTET> mXMPBuffer;
    // The buffer that holds EXIF Data.
    std::vector<JOCTET> mEXIFBuffer;

    // Resolution of the decompressed image.
    size_t mWidth;
    size_t mHeight;
    // Position of EXIF package, default value is -1 which means no EXIF package appears.
    size_t mExifPos;
};
} /* namespace android  */

+2 −0
Original line number Diff line number Diff line
@@ -325,12 +325,14 @@ private:
     *
     * @param compressed_jpeg_image compressed 8-bit JPEG image
     * @param compress_recovery_map compressed recover map
     * @param exif EXIF package
     * @param metadata JPEG/R metadata to encode in XMP of the jpeg
     * @param dest compressed JPEGR image
     * @return NO_ERROR if calculation succeeds, error code if error occurs.
     */
    status_t appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image,
                               jr_compressed_ptr compressed_recovery_map,
                               jr_exif_ptr exif,
                               jr_metadata_ptr metadata,
                               jr_compressed_ptr dest);

+116 −18
Original line number Diff line number Diff line
@@ -26,8 +26,12 @@ using namespace std;

namespace android::recoverymap {

const uint32_t kExifMarker = JPEG_APP0 + 1;
const uint32_t kICCMarker = JPEG_APP0 + 2;
const uint32_t kAPP0Marker = JPEG_APP0;      // JFIF
const uint32_t kAPP1Marker = JPEG_APP0 + 1;  // EXIF, XMP
const uint32_t kAPP2Marker = JPEG_APP0 + 2;  // ICC

const std::string kXmpNameSpace = "http://ns.adobe.com/xap/1.0/";
const std::string kExifIdCode = "Exif";

struct jpegr_source_mgr : jpeg_source_mgr {
    jpegr_source_mgr(const uint8_t* ptr, int len);
@@ -83,6 +87,7 @@ static void jpegrerror_exit(j_common_ptr cinfo) {
}

JpegDecoder::JpegDecoder() {
  mExifPos = 0;
}

JpegDecoder::~JpegDecoder() {
@@ -119,6 +124,13 @@ size_t JpegDecoder::getXMPSize() {
    return mXMPBuffer.size();
}

void* JpegDecoder::getEXIFPtr() {
    return mEXIFBuffer.data();
}

size_t JpegDecoder::getEXIFSize() {
    return mEXIFBuffer.size();
}

size_t JpegDecoder::getDecompressedImageWidth() {
    return mWidth;
@@ -132,7 +144,6 @@ bool JpegDecoder::decode(const void* image, int length) {
    jpeg_decompress_struct cinfo;
    jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
    jpegrerror_mgr myerr;
    string nameSpace = "http://ns.adobe.com/xap/1.0/";

    cinfo.err = jpeg_std_error(&myerr.pub);
    myerr.pub.error_exit = jpegrerror_exit;
@@ -143,26 +154,59 @@ bool JpegDecoder::decode(const void* image, int length) {
    }
    jpeg_create_decompress(&cinfo);

    jpeg_save_markers(&cinfo, kExifMarker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP0Marker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);

    cinfo.src = &mgr;
    jpeg_read_header(&cinfo, TRUE);

    // Save XMP Data
    for (jpeg_marker_struct* marker = cinfo.marker_list; marker; marker = marker->next) {
        if (marker->marker == kExifMarker) {
    // Save XMP data and EXIF data.
    // Here we only handle the first XMP / EXIF package.
    // The parameter pos is used for capturing start offset of EXIF, which is hacky, but working...
    // We assume that all packages are starting with two bytes marker (eg FF E1 for EXIF package),
    // two bytes of package length which is stored in marker->original_length, and the real data
    // which is stored in marker->data. The pos is adding up all previous package lengths (
    // 4 bytes marker and length, marker->original_length) before EXIF appears. Note that here we
    // we are using marker->original_length instead of marker->data_length because in case the real
    // package length is larger than the limitation, jpeg-turbo will only copy the data within the
    // limitation (represented by data_length) and this may vary from original_length / real offset.
    // A better solution is making jpeg_marker_struct holding the offset, but currently it doesn't.
    bool exifAppears = false;
    bool xmpAppears = false;
    size_t pos = 2;  // position after SOI
    for (jpeg_marker_struct* marker = cinfo.marker_list;
         marker && !(exifAppears && xmpAppears);
         marker = marker->next) {

        pos += 4;
        pos += marker->original_length;

        if (marker->marker != kAPP1Marker) {
            continue;
        }

        const unsigned int len = marker->data_length;
            if (len > nameSpace.size() &&
        if (!xmpAppears &&
            len > kXmpNameSpace.size() &&
            !strncmp(reinterpret_cast<const char*>(marker->data),
                         nameSpace.c_str(), nameSpace.size())) {
                     kXmpNameSpace.c_str(),
                     kXmpNameSpace.size())) {
            mXMPBuffer.resize(len+1, 0);
            memcpy(static_cast<void*>(mXMPBuffer.data()), marker->data, len);
                break;
            }
            xmpAppears = true;
        } else if (!exifAppears &&
                   len > kExifIdCode.size() &&
                   !strncmp(reinterpret_cast<const char*>(marker->data),
                            kExifIdCode.c_str(),
                            kExifIdCode.size())) {
            mEXIFBuffer.resize(len, 0);
            memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
            exifAppears = true;
            mExifPos = pos - marker->original_length;
        }
    }


    mWidth = cinfo.image_width;
    mHeight = cinfo.image_height;

@@ -189,6 +233,60 @@ bool JpegDecoder::decode(const void* image, int length) {
    return true;
}

// TODO (Fyodor/Dichen): merge this method with getCompressedImageParameters() since they have
// similar functionality. Yet Dichen is not familiar with who's calling
// getCompressedImageParameters(), looks like it's used by some pending CLs.
bool JpegDecoder::extractEXIF(const void* image, int length) {
    jpeg_decompress_struct cinfo;
    jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
    jpegrerror_mgr myerr;

    cinfo.err = jpeg_std_error(&myerr.pub);
    myerr.pub.error_exit = jpegrerror_exit;

    if (setjmp(myerr.setjmp_buffer)) {
        jpeg_destroy_decompress(&cinfo);
        return false;
    }
    jpeg_create_decompress(&cinfo);

    jpeg_save_markers(&cinfo, kAPP0Marker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);

    cinfo.src = &mgr;
    jpeg_read_header(&cinfo, TRUE);

    bool exifAppears = false;
    size_t pos = 2;  // position after SOI
    for (jpeg_marker_struct* marker = cinfo.marker_list;
         marker && !exifAppears;
         marker = marker->next) {

        pos += 4;
        pos += marker->original_length;

        if (marker->marker != kAPP1Marker) {
            continue;
        }

        const unsigned int len = marker->data_length;
        if (!exifAppears &&
            len > kExifIdCode.size() &&
            !strncmp(reinterpret_cast<const char*>(marker->data),
                     kExifIdCode.c_str(),
                     kExifIdCode.size())) {
            mEXIFBuffer.resize(len, 0);
            memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
            exifAppears = true;
            mExifPos = pos - marker->original_length;
        }
    }

    jpeg_destroy_decompress(&cinfo);
    return true;
}

bool JpegDecoder::decompress(jpeg_decompress_struct* cinfo, const uint8_t* dest,
        bool isSingleChannel) {
    if (isSingleChannel) {
@@ -212,8 +310,8 @@ bool JpegDecoder::getCompressedImageParameters(const void* image, int length,
    }
    jpeg_create_decompress(&cinfo);

    jpeg_save_markers(&cinfo, kExifMarker, 0xFFFF);
    jpeg_save_markers(&cinfo, kICCMarker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
    jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);

    cinfo.src = &mgr;
    if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
+258 −32
Original line number Diff line number Diff line
@@ -82,12 +82,122 @@ status_t Write(jr_compressed_ptr destination, const void* source, size_t length,
  return NO_ERROR;
}

status_t Write(jr_exif_ptr destination, const void* source, size_t length, int &position) {
  memcpy((uint8_t*)destination->data + sizeof(uint8_t) * position, source, length);
  position += length;
  return NO_ERROR;
}

// If the EXIF package doesn't exist in the input JPEG, we'll create one with one entry
// where the length is represented by this value.
const size_t PSEUDO_EXIF_PACKAGE_LENGTH = 28;
// If the EXIF package exists in the input JPEG, we'll add an "JR" entry where the length is
// represented by this value.
const size_t EXIF_J_R_ENTRY_LENGTH = 12;

/*
 * Helper function
 * Add J R entry to existing exif, or create a new one with J R entry if it's null.
 * EXIF syntax / change:
 * ori:
 * FF E1 - APP1
 * 01 FC - size of APP1 (to be calculated)
 * -----------------------------------------------------
 * 45 78 69 66 00 00 - Exif\0\0 "Exif header"
 * 49 49 2A 00 - TIFF Header
 * 08 00 00 00 - offset to the IFD (image file directory)
 * 06 00 - 6 entries
 * 00 01 - Width Tag
 * 03 00 - 'Short' type
 * 01 00 00 00 - one entry
 * 00 05 00 00 - image with 0x500
 *--------------------------------------------------------------------------
 * new:
 * FF E1 - APP1
 * 02 08 - new size, equals to old size + EXIF_J_R_ENTRY_LENGTH (12)
 *-----------------------------------------------------
 * 45 78 69 66 00 00 - Exif\0\0 "Exif header"
 * 49 49 2A 00 - TIFF Header
 * 08 00 00 00 - offset to the IFD (image file directory)
 * 07 00 - +1 entry
 * 4A 52   Custom ('J''R') Tag
 * 07 00 - Unknown type
 * 01 00 00 00 - one element
 * 00 00 00 00 - empty data
 * 00 01 - Width Tag
 * 03 00 - 'Short' type
 * 01 00 00 00 - one entry
 * 00 05 00 00 - image with 0x500
 */
status_t updateExif(jr_exif_ptr exif, jr_exif_ptr dest) {
  if (exif == nullptr || exif->data == nullptr) {
    uint8_t data[PSEUDO_EXIF_PACKAGE_LENGTH] = {
        0x45, 0x78, 0x69, 0x66, 0x00, 0x00,
        0x49, 0x49, 0x2A, 0x00,
        0x08, 0x00, 0x00, 0x00,
        0x01, 0x00,
        0x4A, 0x52,
        0x07, 0x00,
        0x01, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00};
    int pos = 0;
    Write(dest, data, PSEUDO_EXIF_PACKAGE_LENGTH, pos);
    return NO_ERROR;
  }

  int num_entry = 0;
  uint8_t num_entry_low = 0;
  uint8_t num_entry_high = 0;
  bool use_big_endian = false;
  if (reinterpret_cast<uint16_t*>(exif->data)[3] == 0x4949) {
      num_entry_low = reinterpret_cast<uint8_t*>(exif->data)[14];
      num_entry_high = reinterpret_cast<uint8_t*>(exif->data)[15];
  } else if (reinterpret_cast<uint16_t*>(exif->data)[3] == 0x4d4d) {
      use_big_endian = true;
      num_entry_high = reinterpret_cast<uint8_t*>(exif->data)[14];
      num_entry_low = reinterpret_cast<uint8_t*>(exif->data)[15];
  } else {
      return ERROR_JPEGR_METADATA_ERROR;
  }
  num_entry = (num_entry_high << 8) | num_entry_low;
  num_entry += 1;
  num_entry_low = num_entry & 0xff;
  num_entry_high = (num_entry << 8) & 0xff;

  int pos = 0;
  Write(dest, (uint8_t*)exif->data, 14, pos);

  if (use_big_endian) {
    Write(dest, &num_entry_high, 1, pos);
    Write(dest, &num_entry_low, 1, pos);
    uint8_t data[EXIF_J_R_ENTRY_LENGTH] = {
          0x4A, 0x52,
          0x07, 0x00,
          0x01, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00};
    Write(dest, data, EXIF_J_R_ENTRY_LENGTH, pos);
  } else {
    Write(dest, &num_entry_low, 1, pos);
    Write(dest, &num_entry_high, 1, pos);
    uint8_t data[EXIF_J_R_ENTRY_LENGTH] = {
          0x4A, 0x52,
          0x00, 0x07,
          0x00, 0x00, 0x00, 0x01,
          0x00, 0x00, 0x00, 0x00};
    Write(dest, data, EXIF_J_R_ENTRY_LENGTH, pos);
  }

  Write(dest, (uint8_t*)exif->data + 16, exif->length - 16, pos);

  return NO_ERROR;
}

/* Encode API-0 */
status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
                                  jpegr_transfer_function hdr_tf,
                                  jr_compressed_ptr dest,
                                  int quality,
                                  jr_exif_ptr /* exif */) {
                                  jr_exif_ptr exif) {
  if (uncompressed_p010_image == nullptr || dest == nullptr) {
    return ERROR_JPEGR_INVALID_NULL_PTR;
  }
@@ -129,7 +239,18 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
  jpeg.data = jpeg_encoder.getCompressedImagePtr();
  jpeg.length = jpeg_encoder.getCompressedImageSize();

  JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, &metadata, dest));
  jpegr_exif_struct new_exif;
  if (exif->data == nullptr) {
      new_exif.length = PSEUDO_EXIF_PACKAGE_LENGTH;
  } else {
      new_exif.length = exif->length + EXIF_J_R_ENTRY_LENGTH;
  }
  new_exif.data = new uint8_t[new_exif.length];
  std::unique_ptr<uint8_t[]> new_exif_data;
  new_exif_data.reset(reinterpret_cast<uint8_t*>(new_exif.data));
  JPEGR_CHECK(updateExif(exif, &new_exif));

  JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, &new_exif, &metadata, dest));

  return NO_ERROR;
}
@@ -140,7 +261,7 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
                                  jpegr_transfer_function hdr_tf,
                                  jr_compressed_ptr dest,
                                  int quality,
                                  jr_exif_ptr /* exif */) {
                                  jr_exif_ptr exif) {
  if (uncompressed_p010_image == nullptr
   || uncompressed_yuv_420_image == nullptr
   || dest == nullptr) {
@@ -186,7 +307,19 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
  jpeg.data = jpeg_encoder.getCompressedImagePtr();
  jpeg.length = jpeg_encoder.getCompressedImageSize();

  JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, &metadata, dest));
  jpegr_exif_struct new_exif;
  if (exif == nullptr || exif->data == nullptr) {
      new_exif.length = PSEUDO_EXIF_PACKAGE_LENGTH;
  } else {
      new_exif.length = exif->length + EXIF_J_R_ENTRY_LENGTH;
  }

  new_exif.data = new uint8_t[new_exif.length];
  std::unique_ptr<uint8_t[]> new_exif_data;
  new_exif_data.reset(reinterpret_cast<uint8_t*>(new_exif.data));
  JPEGR_CHECK(updateExif(exif, &new_exif));

  JPEGR_CHECK(appendRecoveryMap(&jpeg, &compressed_map, &new_exif, &metadata, dest));

  return NO_ERROR;
}
@@ -228,7 +361,41 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
  compressed_map.data = compressed_map_data.get();
  JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map));

  JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, &metadata, dest));
  // Extract EXIF from JPEG without decoding.
  JpegDecoder jpeg_decoder;
  if (!jpeg_decoder.extractEXIF(compressed_jpeg_image->data, compressed_jpeg_image->length)) {
    return ERROR_JPEGR_DECODE_ERROR;
  }

  jpegr_exif_struct exif;
  exif.data = nullptr;
  exif.length = 0;
  // Delete EXIF package if it appears, and update exif.
  if (jpeg_decoder.getEXIFPos() != 0) {
    int new_length = compressed_jpeg_image->length - jpeg_decoder.getEXIFSize() - 4;
    memcpy((uint8_t*)compressed_jpeg_image->data + jpeg_decoder.getEXIFPos() - 4,
           (uint8_t*)compressed_jpeg_image->data + jpeg_decoder.getEXIFPos()
                  + jpeg_decoder.getEXIFSize(),
           compressed_jpeg_image->length - jpeg_decoder.getEXIFPos() - jpeg_decoder.getEXIFSize());
    compressed_jpeg_image->length = new_length;
    exif.data = jpeg_decoder.getEXIFPtr();
    exif.length = jpeg_decoder.getEXIFSize();
  }

  jpegr_exif_struct new_exif;
  if (exif.data == nullptr) {
      new_exif.length = PSEUDO_EXIF_PACKAGE_LENGTH;
  } else {
      new_exif.length = exif.length + EXIF_J_R_ENTRY_LENGTH;
  }

  new_exif.data = new uint8_t[new_exif.length];
  std::unique_ptr<uint8_t[]> new_exif_data;
  new_exif_data.reset(reinterpret_cast<uint8_t*>(new_exif.data));
  JPEGR_CHECK(updateExif(&exif, &new_exif));

  JPEGR_CHECK(appendRecoveryMap(
          compressed_jpeg_image, &compressed_map, &new_exif, &metadata, dest));

  return NO_ERROR;
}
@@ -254,6 +421,32 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
  uncompressed_yuv_420_image.height = jpeg_decoder.getDecompressedImageHeight();
  uncompressed_yuv_420_image.colorGamut = compressed_jpeg_image->colorGamut;

  jpegr_exif_struct exif;
  exif.data = nullptr;
  exif.length = 0;
  // Delete EXIF package if it appears, and update exif.
  if (jpeg_decoder.getEXIFPos() != 0) {
    int new_length = compressed_jpeg_image->length - jpeg_decoder.getEXIFSize() - 4;
    memcpy((uint8_t*)compressed_jpeg_image->data + jpeg_decoder.getEXIFPos() - 4,
           (uint8_t*)compressed_jpeg_image->data + jpeg_decoder.getEXIFPos()
                  + jpeg_decoder.getEXIFSize(),
           compressed_jpeg_image->length - jpeg_decoder.getEXIFPos() - jpeg_decoder.getEXIFSize());
    compressed_jpeg_image->length = new_length;
    exif.data = jpeg_decoder.getEXIFPtr();
    exif.length = jpeg_decoder.getEXIFSize();
  }

  jpegr_exif_struct new_exif;
  if (exif.data == nullptr) {
      new_exif.length = PSEUDO_EXIF_PACKAGE_LENGTH;
  } else {
      new_exif.length = exif.length + EXIF_J_R_ENTRY_LENGTH;
  }
  new_exif.data = new uint8_t[new_exif.length];
  std::unique_ptr<uint8_t[]> new_exif_data;
  new_exif_data.reset(reinterpret_cast<uint8_t*>(new_exif.data));
  JPEGR_CHECK(updateExif(&exif, &new_exif));

  if (uncompressed_p010_image->width != uncompressed_yuv_420_image.width
   || uncompressed_p010_image->height != uncompressed_yuv_420_image.height) {
    return ERROR_JPEGR_RESOLUTION_MISMATCH;
@@ -278,7 +471,8 @@ status_t RecoveryMap::encodeJPEGR(jr_uncompressed_ptr uncompressed_p010_image,
  compressed_map.data = compressed_map_data.get();
  JPEGR_CHECK(compressRecoveryMap(&map, &compressed_map));

  JPEGR_CHECK(appendRecoveryMap(compressed_jpeg_image, &compressed_map, &metadata, dest));
  JPEGR_CHECK(appendRecoveryMap(
          compressed_jpeg_image, &compressed_map, &new_exif, &metadata, dest));

  return NO_ERROR;
}
@@ -614,52 +808,84 @@ status_t RecoveryMap::extractRecoveryMap(jr_compressed_ptr compressed_jpegr_imag
  return extractPrimaryImageAndRecoveryMap(compressed_jpegr_image, nullptr, dest);
}

// JPEG/R structure:
// SOI (ff d8)
// APP1 (ff e1)
// 2 bytes of length (2 + length of exif package)
// EXIF package (this includes the first two bytes representing the package length)
// APP1 (ff e1)
// 2 bytes of length (2 + 29 + length of xmp package)
// name space ("http://ns.adobe.com/xap/1.0/\0")
// xmp
// primary image (without the first two bytes (SOI) and without EXIF, may have other packages)
// secondary image (the recovery map)
//
// Metadata versions we are using:
// ECMA TR-98 for JFIF marker
// Exif 2.2 spec for EXIF marker
// Adobe XMP spec part 3 for XMP marker
// ICC v4.3 spec for ICC
status_t RecoveryMap::appendRecoveryMap(jr_compressed_ptr compressed_jpeg_image,
                                        jr_compressed_ptr compressed_recovery_map,
                                        jr_exif_ptr exif,
                                        jr_metadata_ptr metadata,
                                        jr_compressed_ptr dest) {
  if (compressed_jpeg_image == nullptr
   || compressed_recovery_map == nullptr
   || exif == nullptr
   || metadata == nullptr
   || dest == nullptr) {
    return ERROR_JPEGR_INVALID_NULL_PTR;
  }

  int pos = 0;

  // Write SOI
  JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
  JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos));

  // Write EXIF
  {
    const int length = 2 + exif->length;
    const uint8_t lengthH = ((length >> 8) & 0xff);
    const uint8_t lengthL = (length & 0xff);
    JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
    JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos));
    JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
    JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
    JPEGR_CHECK(Write(dest, exif->data, exif->length, pos));
  }

  // Prepare and write XMP
  {
    const string xmp = generateXmp(compressed_recovery_map->length, *metadata);
    const string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
    const int nameSpaceLength = nameSpace.size() + 1;  // need to count the null terminator

  // 2 bytes: APP1 sign (ff e1)
    // 2 bytes: representing the length of the package
    // 29 bytes: length of name space "http://ns.adobe.com/xap/1.0/\0",
    // x bytes: length of xmp packet

    const int length = 3 + nameSpaceLength + xmp.size();
    const uint8_t lengthH = ((length >> 8) & 0xff);
    const uint8_t lengthL = (length & 0xff);

  int pos = 0;

  // JPEG/R structure:
  // SOI (ff d8)
  // APP1 (ff e1)
  // 2 bytes of length (2 + 29 + length of xmp packet)
  // name space ("http://ns.adobe.com/xap/1.0/\0")
  // xmp
  // primary image (without the first two bytes, the SOI sign)
  // secondary image (the recovery map)
  JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
  JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos));
    JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
    JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos));
    JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
    JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
    JPEGR_CHECK(Write(dest, (void*)nameSpace.c_str(), nameSpaceLength, pos));
    JPEGR_CHECK(Write(dest, (void*)xmp.c_str(), xmp.size(), pos));
  }

  // Write primary image
  JPEGR_CHECK(Write(dest,
      (uint8_t*)compressed_jpeg_image->data + 2, compressed_jpeg_image->length - 2, pos));

  // Write secondary image
  JPEGR_CHECK(Write(dest, compressed_recovery_map->data, compressed_recovery_map->length, pos));

  // Set back length
  dest->length = pos;

  // Done!
  return NO_ERROR;
}