aboutsummaryrefslogtreecommitdiff
path: root/lib/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/src')
-rw-r--r--lib/src/gainmapmath.cpp733
-rw-r--r--lib/src/icc.cpp680
-rw-r--r--lib/src/jpegdecoderhelper.cpp537
-rw-r--r--lib/src/jpegencoderhelper.cpp287
-rw-r--r--lib/src/jpegr.cpp1509
-rw-r--r--lib/src/jpegrutils.cpp583
-rw-r--r--lib/src/multipictureformat.cpp92
7 files changed, 4421 insertions, 0 deletions
diff --git a/lib/src/gainmapmath.cpp b/lib/src/gainmapmath.cpp
new file mode 100644
index 0000000..8ace1e7
--- /dev/null
+++ b/lib/src/gainmapmath.cpp
@@ -0,0 +1,733 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ultrahdr/gainmapmath.h"
+
+namespace ultrahdr {
+
+static const std::vector<float> kPqOETF = [] {
+ std::vector<float> result;
+ for (size_t idx = 0; idx < kPqOETFNumEntries; idx++) {
+ float value = static_cast<float>(idx) / static_cast<float>(kPqOETFNumEntries - 1);
+ result.push_back(pqOetf(value));
+ }
+ return result;
+}();
+
+static const std::vector<float> kPqInvOETF = [] {
+ std::vector<float> result;
+ for (size_t idx = 0; idx < kPqInvOETFNumEntries; idx++) {
+ float value = static_cast<float>(idx) / static_cast<float>(kPqInvOETFNumEntries - 1);
+ result.push_back(pqInvOetf(value));
+ }
+ return result;
+}();
+
+static const std::vector<float> kHlgOETF = [] {
+ std::vector<float> result;
+ for (size_t idx = 0; idx < kHlgOETFNumEntries; idx++) {
+ float value = static_cast<float>(idx) / static_cast<float>(kHlgOETFNumEntries - 1);
+ result.push_back(hlgOetf(value));
+ }
+ return result;
+}();
+
+static const std::vector<float> kHlgInvOETF = [] {
+ std::vector<float> result;
+ for (size_t idx = 0; idx < kHlgInvOETFNumEntries; idx++) {
+ float value = static_cast<float>(idx) / static_cast<float>(kHlgInvOETFNumEntries - 1);
+ result.push_back(hlgInvOetf(value));
+ }
+ return result;
+}();
+
+static const std::vector<float> kSrgbInvOETF = [] {
+ std::vector<float> result;
+ for (size_t idx = 0; idx < kSrgbInvOETFNumEntries; idx++) {
+ float value = static_cast<float>(idx) / static_cast<float>(kSrgbInvOETFNumEntries - 1);
+ result.push_back(srgbInvOetf(value));
+ }
+ return result;
+}();
+
+// Use Shepard's method for inverse distance weighting. For more information:
+// en.wikipedia.org/wiki/Inverse_distance_weighting#Shepard's_method
+
+float ShepardsIDW::euclideanDistance(float x1, float x2, float y1, float y2) {
+ return sqrt(((y2 - y1) * (y2 - y1)) + (x2 - x1) * (x2 - x1));
+}
+
+void ShepardsIDW::fillShepardsIDW(float* weights, int incR, int incB) {
+ for (int y = 0; y < mMapScaleFactor; y++) {
+ for (int x = 0; x < mMapScaleFactor; x++) {
+ float pos_x = ((float)x) / mMapScaleFactor;
+ float pos_y = ((float)y) / mMapScaleFactor;
+ int curr_x = floor(pos_x);
+ int curr_y = floor(pos_y);
+ int next_x = curr_x + incR;
+ int next_y = curr_y + incB;
+ float e1_distance = euclideanDistance(pos_x, curr_x, pos_y, curr_y);
+ int index = y * mMapScaleFactor * 4 + x * 4;
+ if (e1_distance == 0) {
+ weights[index++] = 1.f;
+ weights[index++] = 0.f;
+ weights[index++] = 0.f;
+ weights[index++] = 0.f;
+ } else {
+ float e1_weight = 1.f / e1_distance;
+
+ float e2_distance = euclideanDistance(pos_x, curr_x, pos_y, next_y);
+ float e2_weight = 1.f / e2_distance;
+
+ float e3_distance = euclideanDistance(pos_x, next_x, pos_y, curr_y);
+ float e3_weight = 1.f / e3_distance;
+
+ float e4_distance = euclideanDistance(pos_x, next_x, pos_y, next_y);
+ float e4_weight = 1.f / e4_distance;
+
+ float total_weight = e1_weight + e2_weight + e3_weight + e4_weight;
+
+ weights[index++] = e1_weight / total_weight;
+ weights[index++] = e2_weight / total_weight;
+ weights[index++] = e3_weight / total_weight;
+ weights[index++] = e4_weight / total_weight;
+ }
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sRGB transformations
+
+static const float kMaxPixelFloat = 1.0f;
+static float clampPixelFloat(float value) {
+ return (value < 0.0f) ? 0.0f : (value > kMaxPixelFloat) ? kMaxPixelFloat : value;
+}
+
+// See IEC 61966-2-1/Amd 1:2003, Equation F.7.
+static const float kSrgbR = 0.2126f, kSrgbG = 0.7152f, kSrgbB = 0.0722f;
+
+float srgbLuminance(Color e) { return kSrgbR * e.r + kSrgbG * e.g + kSrgbB * e.b; }
+
+// See ITU-R BT.709-6, Section 3.
+// Uses the same coefficients for deriving luma signal as
+// IEC 61966-2-1/Amd 1:2003 states for luminance, so we reuse the luminance
+// function above.
+static const float kSrgbCb = 1.8556f, kSrgbCr = 1.5748f;
+
+Color srgbRgbToYuv(Color e_gamma) {
+ float y_gamma = srgbLuminance(e_gamma);
+ return {{{y_gamma, (e_gamma.b - y_gamma) / kSrgbCb, (e_gamma.r - y_gamma) / kSrgbCr}}};
+}
+
+// See ITU-R BT.709-6, Section 3.
+// Same derivation to BT.2100's YUV->RGB, below. Similar to srgbRgbToYuv, we
+// can reuse the luminance coefficients since they are the same.
+static const float kSrgbGCb = kSrgbB * kSrgbCb / kSrgbG;
+static const float kSrgbGCr = kSrgbR * kSrgbCr / kSrgbG;
+
+Color srgbYuvToRgb(Color e_gamma) {
+ return {{{clampPixelFloat(e_gamma.y + kSrgbCr * e_gamma.v),
+ clampPixelFloat(e_gamma.y - kSrgbGCb * e_gamma.u - kSrgbGCr * e_gamma.v),
+ clampPixelFloat(e_gamma.y + kSrgbCb * e_gamma.u)}}};
+}
+
+// See IEC 61966-2-1/Amd 1:2003, Equations F.5 and F.6.
+float srgbInvOetf(float e_gamma) {
+ if (e_gamma <= 0.04045f) {
+ return e_gamma / 12.92f;
+ } else {
+ return pow((e_gamma + 0.055f) / 1.055f, 2.4);
+ }
+}
+
+Color srgbInvOetf(Color e_gamma) {
+ return {{{srgbInvOetf(e_gamma.r), srgbInvOetf(e_gamma.g), srgbInvOetf(e_gamma.b)}}};
+}
+
+// See IEC 61966-2-1, Equations F.5 and F.6.
+float srgbInvOetfLUT(float e_gamma) {
+ uint32_t value = static_cast<uint32_t>(e_gamma * (kSrgbInvOETFNumEntries - 1) + 0.5);
+ // TODO() : Remove once conversion modules have appropriate clamping in place
+ value = CLIP3(value, 0, kSrgbInvOETFNumEntries - 1);
+ return kSrgbInvOETF[value];
+}
+
+Color srgbInvOetfLUT(Color e_gamma) {
+ return {{{srgbInvOetfLUT(e_gamma.r), srgbInvOetfLUT(e_gamma.g), srgbInvOetfLUT(e_gamma.b)}}};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Display-P3 transformations
+
+// See SMPTE EG 432-1, Equation 7-8.
+static const float kP3R = 0.20949f, kP3G = 0.72160f, kP3B = 0.06891f;
+
+float p3Luminance(Color e) { return kP3R * e.r + kP3G * e.g + kP3B * e.b; }
+
+// See ITU-R BT.601-7, Sections 2.5.1 and 2.5.2.
+// Unfortunately, calculation of luma signal differs from calculation of
+// luminance for Display-P3, so we can't reuse p3Luminance here.
+static const float kP3YR = 0.299f, kP3YG = 0.587f, kP3YB = 0.114f;
+static const float kP3Cb = 1.772f, kP3Cr = 1.402f;
+
+Color p3RgbToYuv(Color e_gamma) {
+ float y_gamma = kP3YR * e_gamma.r + kP3YG * e_gamma.g + kP3YB * e_gamma.b;
+ return {{{y_gamma, (e_gamma.b - y_gamma) / kP3Cb, (e_gamma.r - y_gamma) / kP3Cr}}};
+}
+
+// See ITU-R BT.601-7, Sections 2.5.1 and 2.5.2.
+// Same derivation to BT.2100's YUV->RGB, below. Similar to p3RgbToYuv, we must
+// use luma signal coefficients rather than the luminance coefficients.
+static const float kP3GCb = kP3YB * kP3Cb / kP3YG;
+static const float kP3GCr = kP3YR * kP3Cr / kP3YG;
+
+Color p3YuvToRgb(Color e_gamma) {
+ return {{{clampPixelFloat(e_gamma.y + kP3Cr * e_gamma.v),
+ clampPixelFloat(e_gamma.y - kP3GCb * e_gamma.u - kP3GCr * e_gamma.v),
+ clampPixelFloat(e_gamma.y + kP3Cb * e_gamma.u)}}};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// BT.2100 transformations - according to ITU-R BT.2100-2
+
+// See ITU-R BT.2100-2, Table 5, HLG Reference OOTF
+static const float kBt2100R = 0.2627f, kBt2100G = 0.6780f, kBt2100B = 0.0593f;
+
+float bt2100Luminance(Color e) { return kBt2100R * e.r + kBt2100G * e.g + kBt2100B * e.b; }
+
+// See ITU-R BT.2100-2, Table 6, Derivation of colour difference signals.
+// BT.2100 uses the same coefficients for calculating luma signal and luminance,
+// so we reuse the luminance function here.
+static const float kBt2100Cb = 1.8814f, kBt2100Cr = 1.4746f;
+
+Color bt2100RgbToYuv(Color e_gamma) {
+ float y_gamma = bt2100Luminance(e_gamma);
+ return {{{y_gamma, (e_gamma.b - y_gamma) / kBt2100Cb, (e_gamma.r - y_gamma) / kBt2100Cr}}};
+}
+
+// See ITU-R BT.2100-2, Table 6, Derivation of colour difference signals.
+//
+// Similar to bt2100RgbToYuv above, we can reuse the luminance coefficients.
+//
+// Derived by inversing bt2100RgbToYuv. The derivation for R and B are pretty
+// straight forward; we just invert the formulas for U and V above. But deriving
+// the formula for G is a bit more complicated:
+//
+// Start with equation for luminance:
+// Y = kBt2100R * R + kBt2100G * G + kBt2100B * B
+// Solve for G:
+// G = (Y - kBt2100R * R - kBt2100B * B) / kBt2100B
+// Substitute equations for R and B in terms YUV:
+// G = (Y - kBt2100R * (Y + kBt2100Cr * V) - kBt2100B * (Y + kBt2100Cb * U)) / kBt2100B
+// Simplify:
+// G = Y * ((1 - kBt2100R - kBt2100B) / kBt2100G)
+// + U * (kBt2100B * kBt2100Cb / kBt2100G)
+// + V * (kBt2100R * kBt2100Cr / kBt2100G)
+//
+// We then get the following coeficients for calculating G from YUV:
+//
+// Coef for Y = (1 - kBt2100R - kBt2100B) / kBt2100G = 1
+// Coef for U = kBt2100B * kBt2100Cb / kBt2100G = kBt2100GCb = ~0.1645
+// Coef for V = kBt2100R * kBt2100Cr / kBt2100G = kBt2100GCr = ~0.5713
+
+static const float kBt2100GCb = kBt2100B * kBt2100Cb / kBt2100G;
+static const float kBt2100GCr = kBt2100R * kBt2100Cr / kBt2100G;
+
+Color bt2100YuvToRgb(Color e_gamma) {
+ return {{{clampPixelFloat(e_gamma.y + kBt2100Cr * e_gamma.v),
+ clampPixelFloat(e_gamma.y - kBt2100GCb * e_gamma.u - kBt2100GCr * e_gamma.v),
+ clampPixelFloat(e_gamma.y + kBt2100Cb * e_gamma.u)}}};
+}
+
+// See ITU-R BT.2100-2, Table 5, HLG Reference OETF.
+static const float kHlgA = 0.17883277f, kHlgB = 0.28466892f, kHlgC = 0.55991073;
+
+float hlgOetf(float e) {
+ if (e <= 1.0f / 12.0f) {
+ return sqrt(3.0f * e);
+ } else {
+ return kHlgA * log(12.0f * e - kHlgB) + kHlgC;
+ }
+}
+
+Color hlgOetf(Color e) { return {{{hlgOetf(e.r), hlgOetf(e.g), hlgOetf(e.b)}}}; }
+
+float hlgOetfLUT(float e) {
+ uint32_t value = static_cast<uint32_t>(e * (kHlgOETFNumEntries - 1) + 0.5);
+ // TODO() : Remove once conversion modules have appropriate clamping in place
+ value = CLIP3(value, 0, kHlgOETFNumEntries - 1);
+
+ return kHlgOETF[value];
+}
+
+Color hlgOetfLUT(Color e) { return {{{hlgOetfLUT(e.r), hlgOetfLUT(e.g), hlgOetfLUT(e.b)}}}; }
+
+// See ITU-R BT.2100-2, Table 5, HLG Reference EOTF.
+float hlgInvOetf(float e_gamma) {
+ if (e_gamma <= 0.5f) {
+ return pow(e_gamma, 2.0f) / 3.0f;
+ } else {
+ return (exp((e_gamma - kHlgC) / kHlgA) + kHlgB) / 12.0f;
+ }
+}
+
+Color hlgInvOetf(Color e_gamma) {
+ return {{{hlgInvOetf(e_gamma.r), hlgInvOetf(e_gamma.g), hlgInvOetf(e_gamma.b)}}};
+}
+
+float hlgInvOetfLUT(float e_gamma) {
+ uint32_t value = static_cast<uint32_t>(e_gamma * (kHlgInvOETFNumEntries - 1) + 0.5);
+ // TODO() : Remove once conversion modules have appropriate clamping in place
+ value = CLIP3(value, 0, kHlgInvOETFNumEntries - 1);
+
+ return kHlgInvOETF[value];
+}
+
+Color hlgInvOetfLUT(Color e_gamma) {
+ return {{{hlgInvOetfLUT(e_gamma.r), hlgInvOetfLUT(e_gamma.g), hlgInvOetfLUT(e_gamma.b)}}};
+}
+
+// See ITU-R BT.2100-2, Table 4, Reference PQ OETF.
+static const float kPqM1 = 2610.0f / 16384.0f, kPqM2 = 2523.0f / 4096.0f * 128.0f;
+static const float kPqC1 = 3424.0f / 4096.0f, kPqC2 = 2413.0f / 4096.0f * 32.0f,
+ kPqC3 = 2392.0f / 4096.0f * 32.0f;
+
+float pqOetf(float e) {
+ if (e <= 0.0f) return 0.0f;
+ return pow((kPqC1 + kPqC2 * pow(e, kPqM1)) / (1 + kPqC3 * pow(e, kPqM1)), kPqM2);
+}
+
+Color pqOetf(Color e) { return {{{pqOetf(e.r), pqOetf(e.g), pqOetf(e.b)}}}; }
+
+float pqOetfLUT(float e) {
+ uint32_t value = static_cast<uint32_t>(e * (kPqOETFNumEntries - 1) + 0.5);
+ // TODO() : Remove once conversion modules have appropriate clamping in place
+ value = CLIP3(value, 0, kPqOETFNumEntries - 1);
+
+ return kPqOETF[value];
+}
+
+Color pqOetfLUT(Color e) { return {{{pqOetfLUT(e.r), pqOetfLUT(e.g), pqOetfLUT(e.b)}}}; }
+
+// Derived from the inverse of the Reference PQ OETF.
+static const float kPqInvA = 128.0f, kPqInvB = 107.0f, kPqInvC = 2413.0f, kPqInvD = 2392.0f,
+ kPqInvE = 6.2773946361f, kPqInvF = 0.0126833f;
+
+float pqInvOetf(float e_gamma) {
+ // This equation blows up if e_gamma is 0.0, and checking on <= 0.0 doesn't
+ // always catch 0.0. So, check on 0.0001, since anything this small will
+ // effectively be crushed to zero anyways.
+ if (e_gamma <= 0.0001f) return 0.0f;
+ return pow(
+ (kPqInvA * pow(e_gamma, kPqInvF) - kPqInvB) / (kPqInvC - kPqInvD * pow(e_gamma, kPqInvF)),
+ kPqInvE);
+}
+
+Color pqInvOetf(Color e_gamma) {
+ return {{{pqInvOetf(e_gamma.r), pqInvOetf(e_gamma.g), pqInvOetf(e_gamma.b)}}};
+}
+
+float pqInvOetfLUT(float e_gamma) {
+ uint32_t value = static_cast<uint32_t>(e_gamma * (kPqInvOETFNumEntries - 1) + 0.5);
+ // TODO() : Remove once conversion modules have appropriate clamping in place
+ value = CLIP3(value, 0, kPqInvOETFNumEntries - 1);
+
+ return kPqInvOETF[value];
+}
+
+Color pqInvOetfLUT(Color e_gamma) {
+ return {{{pqInvOetfLUT(e_gamma.r), pqInvOetfLUT(e_gamma.g), pqInvOetfLUT(e_gamma.b)}}};
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Color conversions
+
+Color bt709ToP3(Color e) {
+ return {{{0.82254f * e.r + 0.17755f * e.g + 0.00006f * e.b,
+ 0.03312f * e.r + 0.96684f * e.g + -0.00001f * e.b,
+ 0.01706f * e.r + 0.07240f * e.g + 0.91049f * e.b}}};
+}
+
+Color bt709ToBt2100(Color e) {
+ return {{{0.62740f * e.r + 0.32930f * e.g + 0.04332f * e.b,
+ 0.06904f * e.r + 0.91958f * e.g + 0.01138f * e.b,
+ 0.01636f * e.r + 0.08799f * e.g + 0.89555f * e.b}}};
+}
+
+Color p3ToBt709(Color e) {
+ return {{{1.22482f * e.r + -0.22490f * e.g + -0.00007f * e.b,
+ -0.04196f * e.r + 1.04199f * e.g + 0.00001f * e.b,
+ -0.01961f * e.r + -0.07865f * e.g + 1.09831f * e.b}}};
+}
+
+Color p3ToBt2100(Color e) {
+ return {{{0.75378f * e.r + 0.19862f * e.g + 0.04754f * e.b,
+ 0.04576f * e.r + 0.94177f * e.g + 0.01250f * e.b,
+ -0.00121f * e.r + 0.01757f * e.g + 0.98359f * e.b}}};
+}
+
+Color bt2100ToBt709(Color e) {
+ return {{{1.66045f * e.r + -0.58764f * e.g + -0.07286f * e.b,
+ -0.12445f * e.r + 1.13282f * e.g + -0.00837f * e.b,
+ -0.01811f * e.r + -0.10057f * e.g + 1.11878f * e.b}}};
+}
+
+Color bt2100ToP3(Color e) {
+ return {{{1.34369f * e.r + -0.28223f * e.g + -0.06135f * e.b,
+ -0.06533f * e.r + 1.07580f * e.g + -0.01051f * e.b,
+ 0.00283f * e.r + -0.01957f * e.g + 1.01679f * e.b}}};
+}
+
+// TODO: confirm we always want to convert like this before calculating
+// luminance.
+ColorTransformFn getHdrConversionFn(ultrahdr_color_gamut sdr_gamut,
+ ultrahdr_color_gamut hdr_gamut) {
+ switch (sdr_gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ switch (hdr_gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ return identityConversion;
+ case ULTRAHDR_COLORGAMUT_P3:
+ return p3ToBt709;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ return bt2100ToBt709;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ return nullptr;
+ }
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ switch (hdr_gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ return bt709ToP3;
+ case ULTRAHDR_COLORGAMUT_P3:
+ return identityConversion;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ return bt2100ToP3;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ return nullptr;
+ }
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ switch (hdr_gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ return bt709ToBt2100;
+ case ULTRAHDR_COLORGAMUT_P3:
+ return p3ToBt2100;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ return identityConversion;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ return nullptr;
+ }
+ break;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ return nullptr;
+ }
+ return nullptr;
+}
+
+// All of these conversions are derived from the respective input YUV->RGB conversion followed by
+// the RGB->YUV for the receiving encoding. They are consistent with the RGB<->YUV functions in this
+// file, given that we uses BT.709 encoding for sRGB and BT.601 encoding for Display-P3, to match
+// DataSpace.
+
+Color yuv709To601(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + 0.101579f * e_gamma.u + 0.196076f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.989854f * e_gamma.u + -0.110653f * e_gamma.v,
+ 0.0f * e_gamma.y + -0.072453f * e_gamma.u + 0.983398f * e_gamma.v}}};
+}
+
+Color yuv709To2100(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + -0.016969f * e_gamma.u + 0.096312f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.995306f * e_gamma.u + -0.051192f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.011507f * e_gamma.u + 1.002637f * e_gamma.v}}};
+}
+
+Color yuv601To709(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + -0.118188f * e_gamma.u + -0.212685f * e_gamma.v,
+ 0.0f * e_gamma.y + 1.018640f * e_gamma.u + 0.114618f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.075049f * e_gamma.u + 1.025327f * e_gamma.v}}};
+}
+
+Color yuv601To2100(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + -0.128245f * e_gamma.u + -0.115879f * e_gamma.v,
+ 0.0f * e_gamma.y + 1.010016f * e_gamma.u + 0.061592f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.086969f * e_gamma.u + 1.029350f * e_gamma.v}}};
+}
+
+Color yuv2100To709(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + 0.018149f * e_gamma.u + -0.095132f * e_gamma.v,
+ 0.0f * e_gamma.y + 1.004123f * e_gamma.u + 0.051267f * e_gamma.v,
+ 0.0f * e_gamma.y + -0.011524f * e_gamma.u + 0.996782f * e_gamma.v}}};
+}
+
+Color yuv2100To601(Color e_gamma) {
+ return {{{1.0f * e_gamma.y + 0.117887f * e_gamma.u + 0.105521f * e_gamma.v,
+ 0.0f * e_gamma.y + 0.995211f * e_gamma.u + -0.059549f * e_gamma.v,
+ 0.0f * e_gamma.y + -0.084085f * e_gamma.u + 0.976518f * e_gamma.v}}};
+}
+
+void transformYuv420(jr_uncompressed_ptr image, size_t x_chroma, size_t y_chroma,
+ ColorTransformFn fn) {
+ Color yuv1 = getYuv420Pixel(image, x_chroma * 2, y_chroma * 2);
+ Color yuv2 = getYuv420Pixel(image, x_chroma * 2 + 1, y_chroma * 2);
+ Color yuv3 = getYuv420Pixel(image, x_chroma * 2, y_chroma * 2 + 1);
+ Color yuv4 = getYuv420Pixel(image, x_chroma * 2 + 1, y_chroma * 2 + 1);
+
+ yuv1 = fn(yuv1);
+ yuv2 = fn(yuv2);
+ yuv3 = fn(yuv3);
+ yuv4 = fn(yuv4);
+
+ Color new_uv = (yuv1 + yuv2 + yuv3 + yuv4) / 4.0f;
+
+ size_t pixel_y1_idx = x_chroma * 2 + y_chroma * 2 * image->luma_stride;
+ size_t pixel_y2_idx = (x_chroma * 2 + 1) + y_chroma * 2 * image->luma_stride;
+ size_t pixel_y3_idx = x_chroma * 2 + (y_chroma * 2 + 1) * image->luma_stride;
+ size_t pixel_y4_idx = (x_chroma * 2 + 1) + (y_chroma * 2 + 1) * image->luma_stride;
+
+ uint8_t& y1_uint = reinterpret_cast<uint8_t*>(image->data)[pixel_y1_idx];
+ uint8_t& y2_uint = reinterpret_cast<uint8_t*>(image->data)[pixel_y2_idx];
+ uint8_t& y3_uint = reinterpret_cast<uint8_t*>(image->data)[pixel_y3_idx];
+ uint8_t& y4_uint = reinterpret_cast<uint8_t*>(image->data)[pixel_y4_idx];
+
+ size_t pixel_count = image->chroma_stride * image->height / 2;
+ size_t pixel_uv_idx = x_chroma + y_chroma * (image->chroma_stride);
+
+ uint8_t& u_uint = reinterpret_cast<uint8_t*>(image->chroma_data)[pixel_uv_idx];
+ uint8_t& v_uint = reinterpret_cast<uint8_t*>(image->chroma_data)[pixel_count + pixel_uv_idx];
+
+ y1_uint = static_cast<uint8_t>(CLIP3((yuv1.y * 255.0f + 0.5f), 0, 255));
+ y2_uint = static_cast<uint8_t>(CLIP3((yuv2.y * 255.0f + 0.5f), 0, 255));
+ y3_uint = static_cast<uint8_t>(CLIP3((yuv3.y * 255.0f + 0.5f), 0, 255));
+ y4_uint = static_cast<uint8_t>(CLIP3((yuv4.y * 255.0f + 0.5f), 0, 255));
+
+ u_uint = static_cast<uint8_t>(CLIP3((new_uv.u * 255.0f + 128.0f + 0.5f), 0, 255));
+ v_uint = static_cast<uint8_t>(CLIP3((new_uv.v * 255.0f + 128.0f + 0.5f), 0, 255));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Gain map calculations
+uint8_t encodeGain(float y_sdr, float y_hdr, ultrahdr_metadata_ptr metadata) {
+ return encodeGain(y_sdr, y_hdr, metadata, log2(metadata->minContentBoost),
+ log2(metadata->maxContentBoost));
+}
+
+uint8_t encodeGain(float y_sdr, float y_hdr, ultrahdr_metadata_ptr metadata,
+ float log2MinContentBoost, float log2MaxContentBoost) {
+ float gain = 1.0f;
+ if (y_sdr > 0.0f) {
+ gain = y_hdr / y_sdr;
+ }
+
+ if (gain < metadata->minContentBoost) gain = metadata->minContentBoost;
+ if (gain > metadata->maxContentBoost) gain = metadata->maxContentBoost;
+
+ return static_cast<uint8_t>((log2(gain) - log2MinContentBoost) /
+ (log2MaxContentBoost - log2MinContentBoost) * 255.0f);
+}
+
+Color applyGain(Color e, float gain, ultrahdr_metadata_ptr metadata) {
+ float logBoost =
+ log2(metadata->minContentBoost) * (1.0f - gain) + log2(metadata->maxContentBoost) * gain;
+ float gainFactor = exp2(logBoost);
+ return e * gainFactor;
+}
+
+Color applyGain(Color e, float gain, ultrahdr_metadata_ptr metadata, float displayBoost) {
+ float logBoost =
+ log2(metadata->minContentBoost) * (1.0f - gain) + log2(metadata->maxContentBoost) * gain;
+ float gainFactor = exp2(logBoost * displayBoost / metadata->maxContentBoost);
+ return e * gainFactor;
+}
+
+Color applyGainLUT(Color e, float gain, GainLUT& gainLUT) {
+ float gainFactor = gainLUT.getGainFactor(gain);
+ return e * gainFactor;
+}
+
+Color getYuv420Pixel(jr_uncompressed_ptr image, size_t x, size_t y) {
+ uint8_t* luma_data = reinterpret_cast<uint8_t*>(image->data);
+ size_t luma_stride = image->luma_stride;
+ uint8_t* chroma_data = reinterpret_cast<uint8_t*>(image->chroma_data);
+ size_t chroma_stride = image->chroma_stride;
+
+ size_t offset_cr = chroma_stride * (image->height / 2);
+ size_t pixel_y_idx = x + y * luma_stride;
+ size_t pixel_chroma_idx = x / 2 + (y / 2) * chroma_stride;
+
+ uint8_t y_uint = luma_data[pixel_y_idx];
+ uint8_t u_uint = chroma_data[pixel_chroma_idx];
+ uint8_t v_uint = chroma_data[offset_cr + pixel_chroma_idx];
+
+ // 128 bias for UV given we are using jpeglib; see:
+ // https://github.com/kornelski/libjpeg/blob/master/structure.doc
+ return {{{static_cast<float>(y_uint) / 255.0f, (static_cast<float>(u_uint) - 128.0f) / 255.0f,
+ (static_cast<float>(v_uint) - 128.0f) / 255.0f}}};
+}
+
+Color getP010Pixel(jr_uncompressed_ptr image, size_t x, size_t y) {
+ uint16_t* luma_data = reinterpret_cast<uint16_t*>(image->data);
+ size_t luma_stride = image->luma_stride == 0 ? image->width : image->luma_stride;
+ uint16_t* chroma_data = reinterpret_cast<uint16_t*>(image->chroma_data);
+ size_t chroma_stride = image->chroma_stride;
+
+ size_t pixel_y_idx = y * luma_stride + x;
+ size_t pixel_u_idx = (y >> 1) * chroma_stride + (x & ~0x1);
+ size_t pixel_v_idx = pixel_u_idx + 1;
+
+ uint16_t y_uint = luma_data[pixel_y_idx] >> 6;
+ uint16_t u_uint = chroma_data[pixel_u_idx] >> 6;
+ uint16_t v_uint = chroma_data[pixel_v_idx] >> 6;
+
+ // Conversions include taking narrow-range into account.
+ return {{{(static_cast<float>(y_uint) - 64.0f) / 876.0f,
+ (static_cast<float>(u_uint) - 64.0f) / 896.0f - 0.5f,
+ (static_cast<float>(v_uint) - 64.0f) / 896.0f - 0.5f}}};
+}
+
+typedef Color (*getPixelFn)(jr_uncompressed_ptr, size_t, size_t);
+
+static Color samplePixels(jr_uncompressed_ptr image, size_t map_scale_factor, size_t x, size_t y,
+ getPixelFn get_pixel_fn) {
+ Color e = {{{0.0f, 0.0f, 0.0f}}};
+ for (size_t dy = 0; dy < map_scale_factor; ++dy) {
+ for (size_t dx = 0; dx < map_scale_factor; ++dx) {
+ e += get_pixel_fn(image, x * map_scale_factor + dx, y * map_scale_factor + dy);
+ }
+ }
+
+ return e / static_cast<float>(map_scale_factor * map_scale_factor);
+}
+
+Color sampleYuv420(jr_uncompressed_ptr image, size_t map_scale_factor, size_t x, size_t y) {
+ return samplePixels(image, map_scale_factor, x, y, getYuv420Pixel);
+}
+
+Color sampleP010(jr_uncompressed_ptr image, size_t map_scale_factor, size_t x, size_t y) {
+ return samplePixels(image, map_scale_factor, x, y, getP010Pixel);
+}
+
+// TODO: do we need something more clever for filtering either the map or images
+// to generate the map?
+
+static size_t clamp(const size_t& val, const size_t& low, const size_t& high) {
+ return val < low ? low : (high < val ? high : val);
+}
+
+static float mapUintToFloat(uint8_t map_uint) { return static_cast<float>(map_uint) / 255.0f; }
+
+static float pythDistance(float x_diff, float y_diff) {
+ return sqrt(pow(x_diff, 2.0f) + pow(y_diff, 2.0f));
+}
+
+// TODO: If map_scale_factor is guaranteed to be an integer, then remove the following.
+float sampleMap(jr_uncompressed_ptr map, float map_scale_factor, size_t x, size_t y) {
+ float x_map = static_cast<float>(x) / map_scale_factor;
+ float y_map = static_cast<float>(y) / map_scale_factor;
+
+ size_t x_lower = static_cast<size_t>(floor(x_map));
+ size_t x_upper = x_lower + 1;
+ size_t y_lower = static_cast<size_t>(floor(y_map));
+ size_t y_upper = y_lower + 1;
+
+ x_lower = clamp(x_lower, 0, map->width - 1);
+ x_upper = clamp(x_upper, 0, map->width - 1);
+ y_lower = clamp(y_lower, 0, map->height - 1);
+ y_upper = clamp(y_upper, 0, map->height - 1);
+
+ // Use Shepard's method for inverse distance weighting. For more information:
+ // en.wikipedia.org/wiki/Inverse_distance_weighting#Shepard's_method
+
+ float e1 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_lower * map->width]);
+ float e1_dist =
+ pythDistance(x_map - static_cast<float>(x_lower), y_map - static_cast<float>(y_lower));
+ if (e1_dist == 0.0f) return e1;
+
+ float e2 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_upper * map->width]);
+ float e2_dist =
+ pythDistance(x_map - static_cast<float>(x_lower), y_map - static_cast<float>(y_upper));
+ if (e2_dist == 0.0f) return e2;
+
+ float e3 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_lower * map->width]);
+ float e3_dist =
+ pythDistance(x_map - static_cast<float>(x_upper), y_map - static_cast<float>(y_lower));
+ if (e3_dist == 0.0f) return e3;
+
+ float e4 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_upper * map->width]);
+ float e4_dist =
+ pythDistance(x_map - static_cast<float>(x_upper), y_map - static_cast<float>(y_upper));
+ if (e4_dist == 0.0f) return e2;
+
+ float e1_weight = 1.0f / e1_dist;
+ float e2_weight = 1.0f / e2_dist;
+ float e3_weight = 1.0f / e3_dist;
+ float e4_weight = 1.0f / e4_dist;
+ float total_weight = e1_weight + e2_weight + e3_weight + e4_weight;
+
+ return e1 * (e1_weight / total_weight) + e2 * (e2_weight / total_weight) +
+ e3 * (e3_weight / total_weight) + e4 * (e4_weight / total_weight);
+}
+
+float sampleMap(jr_uncompressed_ptr map, size_t map_scale_factor, size_t x, size_t y,
+ ShepardsIDW& weightTables) {
+ // TODO: If map_scale_factor is guaranteed to be an integer power of 2, then optimize the
+ // following by computing log2(map_scale_factor) once and then using >> log2(map_scale_factor)
+ size_t x_lower = x / map_scale_factor;
+ size_t x_upper = x_lower + 1;
+ size_t y_lower = y / map_scale_factor;
+ size_t y_upper = y_lower + 1;
+
+ x_lower = std::min(x_lower, map->width - 1);
+ x_upper = std::min(x_upper, map->width - 1);
+ y_lower = std::min(y_lower, map->height - 1);
+ y_upper = std::min(y_upper, map->height - 1);
+
+ float e1 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_lower * map->width]);
+ float e2 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_lower + y_upper * map->width]);
+ float e3 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_lower * map->width]);
+ float e4 = mapUintToFloat(reinterpret_cast<uint8_t*>(map->data)[x_upper + y_upper * map->width]);
+
+ // TODO: If map_scale_factor is guaranteed to be an integer power of 2, then optimize the
+ // following by using & (map_scale_factor - 1)
+ int offset_x = x % map_scale_factor;
+ int offset_y = y % map_scale_factor;
+
+ float* weights = weightTables.mWeights;
+ if (x_lower == x_upper && y_lower == y_upper)
+ weights = weightTables.mWeightsC;
+ else if (x_lower == x_upper)
+ weights = weightTables.mWeightsNR;
+ else if (y_lower == y_upper)
+ weights = weightTables.mWeightsNB;
+ weights += offset_y * map_scale_factor * 4 + offset_x * 4;
+
+ return e1 * weights[0] + e2 * weights[1] + e3 * weights[2] + e4 * weights[3];
+}
+
+uint32_t colorToRgba1010102(Color e_gamma) {
+ return (0x3ff & static_cast<uint32_t>(e_gamma.r * 1023.0f)) |
+ ((0x3ff & static_cast<uint32_t>(e_gamma.g * 1023.0f)) << 10) |
+ ((0x3ff & static_cast<uint32_t>(e_gamma.b * 1023.0f)) << 20) |
+ (0x3 << 30); // Set alpha to 1.0
+}
+
+uint64_t colorToRgbaF16(Color e_gamma) {
+ return (uint64_t)floatToHalf(e_gamma.r) | (((uint64_t)floatToHalf(e_gamma.g)) << 16) |
+ (((uint64_t)floatToHalf(e_gamma.b)) << 32) | (((uint64_t)floatToHalf(1.0f)) << 48);
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/icc.cpp b/lib/src/icc.cpp
new file mode 100644
index 0000000..b838660
--- /dev/null
+++ b/lib/src/icc.cpp
@@ -0,0 +1,680 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstring>
+
+#include "ultrahdr/ultrahdrcommon.h"
+#include "ultrahdr/icc.h"
+
+namespace ultrahdr {
+
+static void Matrix3x3_apply(const Matrix3x3* m, float* x) {
+ float y0 = x[0] * m->vals[0][0] + x[1] * m->vals[0][1] + x[2] * m->vals[0][2];
+ float y1 = x[0] * m->vals[1][0] + x[1] * m->vals[1][1] + x[2] * m->vals[1][2];
+ float y2 = x[0] * m->vals[2][0] + x[1] * m->vals[2][1] + x[2] * m->vals[2][2];
+ x[0] = y0;
+ x[1] = y1;
+ x[2] = y2;
+}
+
+bool Matrix3x3_invert(const Matrix3x3* src, Matrix3x3* dst) {
+ double a00 = src->vals[0][0];
+ double a01 = src->vals[1][0];
+ double a02 = src->vals[2][0];
+ double a10 = src->vals[0][1];
+ double a11 = src->vals[1][1];
+ double a12 = src->vals[2][1];
+ double a20 = src->vals[0][2];
+ double a21 = src->vals[1][2];
+ double a22 = src->vals[2][2];
+
+ double b0 = a00 * a11 - a01 * a10;
+ double b1 = a00 * a12 - a02 * a10;
+ double b2 = a01 * a12 - a02 * a11;
+ double b3 = a20;
+ double b4 = a21;
+ double b5 = a22;
+
+ double determinant = b0 * b5 - b1 * b4 + b2 * b3;
+
+ if (determinant == 0) {
+ return false;
+ }
+
+ double invdet = 1.0 / determinant;
+ if (invdet > +FLT_MAX || invdet < -FLT_MAX || !isfinitef_((float)invdet)) {
+ return false;
+ }
+
+ b0 *= invdet;
+ b1 *= invdet;
+ b2 *= invdet;
+ b3 *= invdet;
+ b4 *= invdet;
+ b5 *= invdet;
+
+ dst->vals[0][0] = (float)(a11 * b5 - a12 * b4);
+ dst->vals[1][0] = (float)(a02 * b4 - a01 * b5);
+ dst->vals[2][0] = (float)(+b2);
+ dst->vals[0][1] = (float)(a12 * b3 - a10 * b5);
+ dst->vals[1][1] = (float)(a00 * b5 - a02 * b3);
+ dst->vals[2][1] = (float)(-b1);
+ dst->vals[0][2] = (float)(a10 * b4 - a11 * b3);
+ dst->vals[1][2] = (float)(a01 * b3 - a00 * b4);
+ dst->vals[2][2] = (float)(+b0);
+
+ for (int r = 0; r < 3; ++r)
+ for (int c = 0; c < 3; ++c) {
+ if (!isfinitef_(dst->vals[r][c])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static Matrix3x3 Matrix3x3_concat(const Matrix3x3* A, const Matrix3x3* B) {
+ Matrix3x3 m = {{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}};
+ for (int r = 0; r < 3; r++)
+ for (int c = 0; c < 3; c++) {
+ m.vals[r][c] = A->vals[r][0] * B->vals[0][c] + A->vals[r][1] * B->vals[1][c] +
+ A->vals[r][2] * B->vals[2][c];
+ }
+ return m;
+}
+
+static void float_XYZD50_to_grid16_lab(const float* xyz_float, uint8_t* grid16_lab) {
+ float v[3] = {
+ xyz_float[0] / kD50_x,
+ xyz_float[1] / kD50_y,
+ xyz_float[2] / kD50_z,
+ };
+ for (size_t i = 0; i < 3; ++i) {
+ v[i] = v[i] > 0.008856f ? cbrtf(v[i]) : v[i] * 7.787f + (16 / 116.0f);
+ }
+ const float L = v[1] * 116.0f - 16.0f;
+ const float a = (v[0] - v[1]) * 500.0f;
+ const float b = (v[1] - v[2]) * 200.0f;
+ const float Lab_unorm[3] = {
+ L * (1 / 100.f),
+ (a + 128.0f) * (1 / 255.0f),
+ (b + 128.0f) * (1 / 255.0f),
+ };
+ // This will encode L=1 as 0xFFFF. This matches how skcms will interpret the
+ // table, but the spec appears to indicate that the value should be 0xFF00.
+ // https://crbug.com/skia/13807
+ for (size_t i = 0; i < 3; ++i) {
+ reinterpret_cast<uint16_t*>(grid16_lab)[i] =
+ Endian_SwapBE16(float_round_to_unorm16(Lab_unorm[i]));
+ }
+}
+
+std::string IccHelper::get_desc_string(const ultrahdr_transfer_function tf,
+ const ultrahdr_color_gamut gamut) {
+ std::string result;
+ switch (gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ result += "sRGB";
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ result += "Display P3";
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ result += "Rec2020";
+ break;
+ default:
+ result += "Unknown";
+ break;
+ }
+ result += " Gamut with ";
+ switch (tf) {
+ case ULTRAHDR_TF_SRGB:
+ result += "sRGB";
+ break;
+ case ULTRAHDR_TF_LINEAR:
+ result += "Linear";
+ break;
+ case ULTRAHDR_TF_PQ:
+ result += "PQ";
+ break;
+ case ULTRAHDR_TF_HLG:
+ result += "HLG";
+ break;
+ default:
+ result += "Unknown";
+ break;
+ }
+ result += " Transfer";
+ return result;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_text_tag(const char* text) {
+ uint32_t text_length = strlen(text);
+ uint32_t header[] = {
+ Endian_SwapBE32(kTAG_TextType), // Type signature
+ 0, // Reserved
+ Endian_SwapBE32(1), // Number of records
+ Endian_SwapBE32(12), // Record size (must be 12)
+ Endian_SwapBE32(SetFourByteTag('e', 'n', 'U', 'S')), // English USA
+ Endian_SwapBE32(2 * text_length), // Length of string in bytes
+ Endian_SwapBE32(28), // Offset of string
+ };
+
+ uint32_t total_length = text_length * 2 + sizeof(header);
+ total_length = (((total_length + 2) >> 2) << 2); // 4 aligned
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+
+ if (!dataStruct->write(header, sizeof(header))) {
+ ALOGE("write_text_tag(): error in writing data");
+ return dataStruct;
+ }
+
+ for (size_t i = 0; i < text_length; i++) {
+ // Convert ASCII to big-endian UTF-16.
+ dataStruct->write8(0);
+ dataStruct->write8(text[i]);
+ }
+
+ return dataStruct;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_xyz_tag(float x, float y, float z) {
+ uint32_t data[] = {
+ Endian_SwapBE32(kXYZ_PCSSpace),
+ 0,
+ static_cast<uint32_t>(Endian_SwapBE32(float_round_to_fixed(x))),
+ static_cast<uint32_t>(Endian_SwapBE32(float_round_to_fixed(y))),
+ static_cast<uint32_t>(Endian_SwapBE32(float_round_to_fixed(z))),
+ };
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(sizeof(data));
+ dataStruct->write(&data, sizeof(data));
+ return dataStruct;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_trc_tag(const int table_entries,
+ const void* table_16) {
+ int total_length = 4 + 4 + 4 + table_entries * 2;
+ total_length = (((total_length + 2) >> 2) << 2); // 4 aligned
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+ dataStruct->write32(Endian_SwapBE32(kTAG_CurveType)); // Type
+ dataStruct->write32(0); // Reserved
+ dataStruct->write32(Endian_SwapBE32(table_entries)); // Value count
+ for (int i = 0; i < table_entries; ++i) {
+ uint16_t value = reinterpret_cast<const uint16_t*>(table_16)[i];
+ dataStruct->write16(value);
+ }
+ return dataStruct;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_trc_tag(const TransferFunction& fn) {
+ if (fn.a == 1.f && fn.b == 0.f && fn.c == 0.f && fn.d == 0.f && fn.e == 0.f && fn.f == 0.f) {
+ int total_length = 16;
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+ dataStruct->write32(Endian_SwapBE32(kTAG_ParaCurveType)); // Type
+ dataStruct->write32(0); // Reserved
+ dataStruct->write32(Endian_SwapBE16(kExponential_ParaCurveType));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.g)));
+ return dataStruct;
+ }
+
+ int total_length = 40;
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+ dataStruct->write32(Endian_SwapBE32(kTAG_ParaCurveType)); // Type
+ dataStruct->write32(0); // Reserved
+ dataStruct->write32(Endian_SwapBE16(kGABCDEF_ParaCurveType));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.g)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.a)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.b)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.c)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.d)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.e)));
+ dataStruct->write32(Endian_SwapBE32(float_round_to_fixed(fn.f)));
+ return dataStruct;
+}
+
+float IccHelper::compute_tone_map_gain(const ultrahdr_transfer_function tf, float L) {
+ if (L <= 0.f) {
+ return 1.f;
+ }
+ if (tf == ULTRAHDR_TF_PQ) {
+ // The PQ transfer function will map to the range [0, 1]. Linearly scale
+ // it up to the range [0, 10,000/203]. We will then tone map that back
+ // down to [0, 1].
+ constexpr float kInputMaxLuminance = 10000 / 203.f;
+ constexpr float kOutputMaxLuminance = 1.0;
+ L *= kInputMaxLuminance;
+
+ // Compute the tone map gain which will tone map from 10,000/203 to 1.0.
+ constexpr float kToneMapA = kOutputMaxLuminance / (kInputMaxLuminance * kInputMaxLuminance);
+ constexpr float kToneMapB = 1.f / kOutputMaxLuminance;
+ return kInputMaxLuminance * (1.f + kToneMapA * L) / (1.f + kToneMapB * L);
+ }
+ if (tf == ULTRAHDR_TF_HLG) {
+ // Let Lw be the brightness of the display in nits.
+ constexpr float Lw = 203.f;
+ const float gamma = 1.2f + 0.42f * std::log(Lw / 1000.f) / std::log(10.f);
+ return std::pow(L, gamma - 1.f);
+ }
+ return 1.f;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_cicp_tag(uint32_t color_primaries,
+ uint32_t transfer_characteristics) {
+ int total_length = 12; // 4 + 4 + 1 + 1 + 1 + 1
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+ dataStruct->write32(Endian_SwapBE32(kTAG_cicp)); // Type signature
+ dataStruct->write32(0); // Reserved
+ dataStruct->write8(color_primaries); // Color primaries
+ dataStruct->write8(transfer_characteristics); // Transfer characteristics
+ dataStruct->write8(0); // RGB matrix
+ dataStruct->write8(1); // Full range
+ return dataStruct;
+}
+
+void IccHelper::compute_lut_entry(const Matrix3x3& src_to_XYZD50, float rgb[3]) {
+ // Compute the matrices to convert from source to Rec2020, and from Rec2020 to XYZD50.
+ Matrix3x3 src_to_rec2020;
+ const Matrix3x3 rec2020_to_XYZD50 = kRec2020;
+ {
+ Matrix3x3 XYZD50_to_rec2020;
+ Matrix3x3_invert(&rec2020_to_XYZD50, &XYZD50_to_rec2020);
+ src_to_rec2020 = Matrix3x3_concat(&XYZD50_to_rec2020, &src_to_XYZD50);
+ }
+
+ // Convert the source signal to linear.
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ rgb[i] = pqOetf(rgb[i]);
+ }
+
+ // Convert source gamut to Rec2020.
+ Matrix3x3_apply(&src_to_rec2020, rgb);
+
+ // Compute the luminance of the signal.
+ float L = bt2100Luminance({{{rgb[0], rgb[1], rgb[2]}}});
+
+ // Compute the tone map gain based on the luminance.
+ float tone_map_gain = compute_tone_map_gain(ULTRAHDR_TF_PQ, L);
+
+ // Apply the tone map gain.
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ rgb[i] *= tone_map_gain;
+ }
+
+ // Convert from Rec2020-linear to XYZD50.
+ Matrix3x3_apply(&rec2020_to_XYZD50, rgb);
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_clut(const uint8_t* grid_points,
+ const uint8_t* grid_16) {
+ uint32_t value_count = kNumChannels;
+ for (uint32_t i = 0; i < kNumChannels; ++i) {
+ value_count *= grid_points[i];
+ }
+
+ int total_length = 20 + 2 * value_count;
+ total_length = (((total_length + 2) >> 2) << 2); // 4 aligned
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+
+ for (size_t i = 0; i < 16; ++i) {
+ dataStruct->write8(i < kNumChannels ? grid_points[i] : 0); // Grid size
+ }
+ dataStruct->write8(2); // Grid byte width (always 16-bit)
+ dataStruct->write8(0); // Reserved
+ dataStruct->write8(0); // Reserved
+ dataStruct->write8(0); // Reserved
+
+ for (uint32_t i = 0; i < value_count; ++i) {
+ uint16_t value = reinterpret_cast<const uint16_t*>(grid_16)[i];
+ dataStruct->write16(value);
+ }
+
+ return dataStruct;
+}
+
+std::shared_ptr<DataStruct> IccHelper::write_mAB_or_mBA_tag(uint32_t type, bool has_a_curves,
+ const uint8_t* grid_points,
+ const uint8_t* grid_16) {
+ const size_t b_curves_offset = 32;
+ std::shared_ptr<DataStruct> b_curves_data[kNumChannels];
+ std::shared_ptr<DataStruct> a_curves_data[kNumChannels];
+ size_t clut_offset = 0;
+ std::shared_ptr<DataStruct> clut;
+ size_t a_curves_offset = 0;
+
+ // The "B" curve is required.
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ b_curves_data[i] = write_trc_tag(kLinear_TransFun);
+ }
+
+ // The "A" curve and CLUT are optional.
+ if (has_a_curves) {
+ clut_offset = b_curves_offset;
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ clut_offset += b_curves_data[i]->getLength();
+ }
+ clut = write_clut(grid_points, grid_16);
+
+ a_curves_offset = clut_offset + clut->getLength();
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ a_curves_data[i] = write_trc_tag(kLinear_TransFun);
+ }
+ }
+
+ int total_length = b_curves_offset;
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ total_length += b_curves_data[i]->getLength();
+ }
+ if (has_a_curves) {
+ total_length += clut->getLength();
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ total_length += a_curves_data[i]->getLength();
+ }
+ }
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(total_length);
+ dataStruct->write32(Endian_SwapBE32(type)); // Type signature
+ dataStruct->write32(0); // Reserved
+ dataStruct->write8(kNumChannels); // Input channels
+ dataStruct->write8(kNumChannels); // Output channels
+ dataStruct->write16(0); // Reserved
+ dataStruct->write32(Endian_SwapBE32(b_curves_offset)); // B curve offset
+ dataStruct->write32(Endian_SwapBE32(0)); // Matrix offset (ignored)
+ dataStruct->write32(Endian_SwapBE32(0)); // M curve offset (ignored)
+ dataStruct->write32(Endian_SwapBE32(clut_offset)); // CLUT offset
+ dataStruct->write32(Endian_SwapBE32(a_curves_offset)); // A curve offset
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ if (dataStruct->write(b_curves_data[i]->getData(), b_curves_data[i]->getLength())) {
+ return dataStruct;
+ }
+ }
+ if (has_a_curves) {
+ dataStruct->write(clut->getData(), clut->getLength());
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ dataStruct->write(a_curves_data[i]->getData(), a_curves_data[i]->getLength());
+ }
+ }
+ return dataStruct;
+}
+
+std::shared_ptr<DataStruct> IccHelper::writeIccProfile(ultrahdr_transfer_function tf,
+ ultrahdr_color_gamut gamut) {
+ ICCHeader header;
+
+ std::vector<std::pair<uint32_t, std::shared_ptr<DataStruct>>> tags;
+
+ // Compute profile description tag
+ std::string desc = get_desc_string(tf, gamut);
+
+ tags.emplace_back(kTAG_desc, write_text_tag(desc.c_str()));
+
+ Matrix3x3 toXYZD50;
+ switch (gamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ toXYZD50 = kSRGB;
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ toXYZD50 = kDisplayP3;
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ toXYZD50 = kRec2020;
+ break;
+ default:
+ // Should not fall here.
+ return nullptr;
+ }
+
+ // Compute primaries.
+ {
+ tags.emplace_back(kTAG_rXYZ,
+ write_xyz_tag(toXYZD50.vals[0][0], toXYZD50.vals[1][0], toXYZD50.vals[2][0]));
+ tags.emplace_back(kTAG_gXYZ,
+ write_xyz_tag(toXYZD50.vals[0][1], toXYZD50.vals[1][1], toXYZD50.vals[2][1]));
+ tags.emplace_back(kTAG_bXYZ,
+ write_xyz_tag(toXYZD50.vals[0][2], toXYZD50.vals[1][2], toXYZD50.vals[2][2]));
+ }
+
+ // Compute white point tag (must be D50)
+ tags.emplace_back(kTAG_wtpt, write_xyz_tag(kD50_x, kD50_y, kD50_z));
+
+ // Compute transfer curves.
+ if (tf != ULTRAHDR_TF_PQ) {
+ if (tf == ULTRAHDR_TF_HLG) {
+ std::vector<uint8_t> trc_table;
+ trc_table.resize(kTrcTableSize * 2);
+ for (uint32_t i = 0; i < kTrcTableSize; ++i) {
+ float x = i / (kTrcTableSize - 1.f);
+ float y = hlgOetf(x);
+ y *= compute_tone_map_gain(tf, y);
+ float_to_table16(y, &trc_table[2 * i]);
+ }
+
+ tags.emplace_back(kTAG_rTRC,
+ write_trc_tag(kTrcTableSize, reinterpret_cast<uint8_t*>(trc_table.data())));
+ tags.emplace_back(kTAG_gTRC,
+ write_trc_tag(kTrcTableSize, reinterpret_cast<uint8_t*>(trc_table.data())));
+ tags.emplace_back(kTAG_bTRC,
+ write_trc_tag(kTrcTableSize, reinterpret_cast<uint8_t*>(trc_table.data())));
+ } else {
+ tags.emplace_back(kTAG_rTRC, write_trc_tag(kSRGB_TransFun));
+ tags.emplace_back(kTAG_gTRC, write_trc_tag(kSRGB_TransFun));
+ tags.emplace_back(kTAG_bTRC, write_trc_tag(kSRGB_TransFun));
+ }
+ }
+
+ // Compute CICP.
+ if (tf == ULTRAHDR_TF_HLG || tf == ULTRAHDR_TF_PQ) {
+ // The CICP tag is present in ICC 4.4, so update the header's version.
+ header.version = Endian_SwapBE32(0x04400000);
+
+ uint32_t color_primaries = 0;
+ if (gamut == ULTRAHDR_COLORGAMUT_BT709) {
+ color_primaries = kCICPPrimariesSRGB;
+ } else if (gamut == ULTRAHDR_COLORGAMUT_P3) {
+ color_primaries = kCICPPrimariesP3;
+ }
+
+ uint32_t transfer_characteristics = 0;
+ if (tf == ULTRAHDR_TF_SRGB) {
+ transfer_characteristics = kCICPTrfnSRGB;
+ } else if (tf == ULTRAHDR_TF_LINEAR) {
+ transfer_characteristics = kCICPTrfnLinear;
+ } else if (tf == ULTRAHDR_TF_PQ) {
+ transfer_characteristics = kCICPTrfnPQ;
+ } else if (tf == ULTRAHDR_TF_HLG) {
+ transfer_characteristics = kCICPTrfnHLG;
+ }
+ tags.emplace_back(kTAG_cicp, write_cicp_tag(color_primaries, transfer_characteristics));
+ }
+
+ // Compute A2B0.
+ if (tf == ULTRAHDR_TF_PQ) {
+ std::vector<uint8_t> a2b_grid;
+ a2b_grid.resize(kGridSize * kGridSize * kGridSize * kNumChannels * 2);
+ size_t a2b_grid_index = 0;
+ for (uint32_t r_index = 0; r_index < kGridSize; ++r_index) {
+ for (uint32_t g_index = 0; g_index < kGridSize; ++g_index) {
+ for (uint32_t b_index = 0; b_index < kGridSize; ++b_index) {
+ float rgb[3] = {
+ r_index / (kGridSize - 1.f),
+ g_index / (kGridSize - 1.f),
+ b_index / (kGridSize - 1.f),
+ };
+ compute_lut_entry(toXYZD50, rgb);
+ float_XYZD50_to_grid16_lab(rgb, &a2b_grid[a2b_grid_index]);
+ a2b_grid_index += 6;
+ }
+ }
+ }
+ const uint8_t* grid_16 = reinterpret_cast<const uint8_t*>(a2b_grid.data());
+
+ uint8_t grid_points[kNumChannels];
+ for (size_t i = 0; i < kNumChannels; ++i) {
+ grid_points[i] = kGridSize;
+ }
+
+ auto a2b_data = write_mAB_or_mBA_tag(kTAG_mABType,
+ /* has_a_curves */ true, grid_points, grid_16);
+ tags.emplace_back(kTAG_A2B0, std::move(a2b_data));
+ }
+
+ // Compute B2A0.
+ if (tf == ULTRAHDR_TF_PQ) {
+ auto b2a_data = write_mAB_or_mBA_tag(kTAG_mBAType,
+ /* has_a_curves */ false,
+ /* grid_points */ nullptr,
+ /* grid_16 */ nullptr);
+ tags.emplace_back(kTAG_B2A0, std::move(b2a_data));
+ }
+
+ // Compute copyright tag
+ tags.emplace_back(kTAG_cprt, write_text_tag("Google Inc. 2022"));
+
+ // Compute the size of the profile.
+ size_t tag_data_size = 0;
+ for (const auto& tag : tags) {
+ tag_data_size += tag.second->getLength();
+ }
+ size_t tag_table_size = kICCTagTableEntrySize * tags.size();
+ size_t profile_size = kICCHeaderSize + tag_table_size + tag_data_size;
+
+ std::shared_ptr<DataStruct> dataStruct =
+ std::make_shared<DataStruct>(profile_size + kICCIdentifierSize);
+
+ // Write identifier, chunk count, and chunk ID
+ if (!dataStruct->write(kICCIdentifier, sizeof(kICCIdentifier)) || !dataStruct->write8(1) ||
+ !dataStruct->write8(1)) {
+ ALOGE("writeIccProfile(): error in identifier");
+ return dataStruct;
+ }
+
+ // Write the header.
+ header.data_color_space = Endian_SwapBE32(Signature_RGB);
+ header.pcs = Endian_SwapBE32(tf == ULTRAHDR_TF_PQ ? Signature_Lab : Signature_XYZ);
+ header.size = Endian_SwapBE32(profile_size);
+ header.tag_count = Endian_SwapBE32(tags.size());
+
+ if (!dataStruct->write(&header, sizeof(header))) {
+ ALOGE("writeIccProfile(): error in header");
+ return dataStruct;
+ }
+
+ // Write the tag table. Track the offset and size of the previous tag to
+ // compute each tag's offset. An empty SkData indicates that the previous
+ // tag is to be reused.
+ uint32_t last_tag_offset = sizeof(header) + tag_table_size;
+ uint32_t last_tag_size = 0;
+ for (const auto& tag : tags) {
+ last_tag_offset = last_tag_offset + last_tag_size;
+ last_tag_size = tag.second->getLength();
+ uint32_t tag_table_entry[3] = {
+ Endian_SwapBE32(tag.first),
+ Endian_SwapBE32(last_tag_offset),
+ Endian_SwapBE32(last_tag_size),
+ };
+ if (!dataStruct->write(tag_table_entry, sizeof(tag_table_entry))) {
+ ALOGE("writeIccProfile(): error in writing tag table");
+ return dataStruct;
+ }
+ }
+
+ // Write the tags.
+ for (const auto& tag : tags) {
+ if (!dataStruct->write(tag.second->getData(), tag.second->getLength())) {
+ ALOGE("writeIccProfile(): error in writing tags");
+ return dataStruct;
+ }
+ }
+
+ return dataStruct;
+}
+
+bool IccHelper::tagsEqualToMatrix(const Matrix3x3& matrix, const uint8_t* red_tag,
+ const uint8_t* green_tag, const uint8_t* blue_tag) {
+ std::shared_ptr<DataStruct> red_tag_test =
+ write_xyz_tag(matrix.vals[0][0], matrix.vals[1][0], matrix.vals[2][0]);
+ std::shared_ptr<DataStruct> green_tag_test =
+ write_xyz_tag(matrix.vals[0][1], matrix.vals[1][1], matrix.vals[2][1]);
+ std::shared_ptr<DataStruct> blue_tag_test =
+ write_xyz_tag(matrix.vals[0][2], matrix.vals[1][2], matrix.vals[2][2]);
+ return memcmp(red_tag, red_tag_test->getData(), kColorantTagSize) == 0 &&
+ memcmp(green_tag, green_tag_test->getData(), kColorantTagSize) == 0 &&
+ memcmp(blue_tag, blue_tag_test->getData(), kColorantTagSize) == 0;
+}
+
+ultrahdr_color_gamut IccHelper::readIccColorGamut(void* icc_data, size_t icc_size) {
+ // Each tag table entry consists of 3 fields of 4 bytes each.
+ static const size_t kTagTableEntrySize = 12;
+
+ if (icc_data == nullptr || icc_size < sizeof(ICCHeader) + kICCIdentifierSize) {
+ return ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+ }
+
+ if (memcmp(icc_data, kICCIdentifier, sizeof(kICCIdentifier)) != 0) {
+ return ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+ }
+
+ uint8_t* icc_bytes = reinterpret_cast<uint8_t*>(icc_data) + kICCIdentifierSize;
+
+ ICCHeader* header = reinterpret_cast<ICCHeader*>(icc_bytes);
+
+ // Use 0 to indicate not found, since offsets are always relative to start
+ // of ICC data and therefore a tag offset of zero would never be valid.
+ size_t red_primary_offset = 0, green_primary_offset = 0, blue_primary_offset = 0;
+ size_t red_primary_size = 0, green_primary_size = 0, blue_primary_size = 0;
+ for (size_t tag_idx = 0; tag_idx < Endian_SwapBE32(header->tag_count); ++tag_idx) {
+ uint32_t* tag_entry_start =
+ reinterpret_cast<uint32_t*>(icc_bytes + sizeof(ICCHeader) + tag_idx * kTagTableEntrySize);
+ // first 4 bytes are the tag signature, next 4 bytes are the tag offset,
+ // last 4 bytes are the tag length in bytes.
+ if (red_primary_offset == 0 && *tag_entry_start == Endian_SwapBE32(kTAG_rXYZ)) {
+ red_primary_offset = Endian_SwapBE32(*(tag_entry_start + 1));
+ red_primary_size = Endian_SwapBE32(*(tag_entry_start + 2));
+ } else if (green_primary_offset == 0 && *tag_entry_start == Endian_SwapBE32(kTAG_gXYZ)) {
+ green_primary_offset = Endian_SwapBE32(*(tag_entry_start + 1));
+ green_primary_size = Endian_SwapBE32(*(tag_entry_start + 2));
+ } else if (blue_primary_offset == 0 && *tag_entry_start == Endian_SwapBE32(kTAG_bXYZ)) {
+ blue_primary_offset = Endian_SwapBE32(*(tag_entry_start + 1));
+ blue_primary_size = Endian_SwapBE32(*(tag_entry_start + 2));
+ }
+ }
+
+ if (red_primary_offset == 0 || red_primary_size != kColorantTagSize ||
+ kICCIdentifierSize + red_primary_offset + red_primary_size > icc_size ||
+ green_primary_offset == 0 || green_primary_size != kColorantTagSize ||
+ kICCIdentifierSize + green_primary_offset + green_primary_size > icc_size ||
+ blue_primary_offset == 0 || blue_primary_size != kColorantTagSize ||
+ kICCIdentifierSize + blue_primary_offset + blue_primary_size > icc_size) {
+ return ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+ }
+
+ uint8_t* red_tag = icc_bytes + red_primary_offset;
+ uint8_t* green_tag = icc_bytes + green_primary_offset;
+ uint8_t* blue_tag = icc_bytes + blue_primary_offset;
+
+ // Serialize tags as we do on encode and compare what we find to that to
+ // determine the gamut (since we don't have a need yet for full deserialize).
+ if (tagsEqualToMatrix(kSRGB, red_tag, green_tag, blue_tag)) {
+ return ULTRAHDR_COLORGAMUT_BT709;
+ } else if (tagsEqualToMatrix(kDisplayP3, red_tag, green_tag, blue_tag)) {
+ return ULTRAHDR_COLORGAMUT_P3;
+ } else if (tagsEqualToMatrix(kRec2020, red_tag, green_tag, blue_tag)) {
+ return ULTRAHDR_COLORGAMUT_BT2100;
+ }
+
+ // Didn't find a match to one of the profiles we write; indicate the gamut
+ // is unspecified since we don't understand it.
+ return ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/jpegdecoderhelper.cpp b/lib/src/jpegdecoderhelper.cpp
new file mode 100644
index 0000000..8a8278d
--- /dev/null
+++ b/lib/src/jpegdecoderhelper.cpp
@@ -0,0 +1,537 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <errno.h>
+#include <setjmp.h>
+
+#include <cstring>
+
+#include "ultrahdr/ultrahdrcommon.h"
+#include "ultrahdr/ultrahdr.h"
+#include "ultrahdr/jpegdecoderhelper.h"
+
+using namespace std;
+
+namespace ultrahdr {
+
+const uint32_t kAPP0Marker = JPEG_APP0; // JFIF
+const uint32_t kAPP1Marker = JPEG_APP0 + 1; // EXIF, XMP
+const uint32_t kAPP2Marker = JPEG_APP0 + 2; // ICC
+
+constexpr uint32_t kICCMarkerHeaderSize = 14;
+constexpr uint8_t kICCSig[] = {
+ 'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', '\0',
+};
+constexpr uint8_t kXmpNameSpace[] = {
+ 'h', 't', 't', 'p', ':', '/', '/', 'n', 's', '.', 'a', 'd', 'o', 'b', 'e',
+ '.', 'c', 'o', 'm', '/', 'x', 'a', 'p', '/', '1', '.', '0', '/', '\0',
+};
+constexpr uint8_t kExifIdCode[] = {
+ 'E', 'x', 'i', 'f', '\0', '\0',
+};
+
+struct jpegr_source_mgr : jpeg_source_mgr {
+ jpegr_source_mgr(const uint8_t* ptr, int len);
+ ~jpegr_source_mgr();
+
+ const uint8_t* mBufferPtr;
+ size_t mBufferLength;
+};
+
+struct jpegrerror_mgr {
+ struct jpeg_error_mgr pub;
+ jmp_buf setjmp_buffer;
+};
+
+static void jpegr_init_source(j_decompress_ptr cinfo) {
+ jpegr_source_mgr* src = static_cast<jpegr_source_mgr*>(cinfo->src);
+ src->next_input_byte = static_cast<const JOCTET*>(src->mBufferPtr);
+ src->bytes_in_buffer = src->mBufferLength;
+}
+
+static boolean jpegr_fill_input_buffer(j_decompress_ptr /* cinfo */) {
+ ALOGE("%s : should not get here", __func__);
+ return FALSE;
+}
+
+static void jpegr_skip_input_data(j_decompress_ptr cinfo, long num_bytes) {
+ jpegr_source_mgr* src = static_cast<jpegr_source_mgr*>(cinfo->src);
+
+ if (num_bytes > static_cast<long>(src->bytes_in_buffer)) {
+ ALOGE("jpegr_skip_input_data - num_bytes > (long)src->bytes_in_buffer");
+ } else {
+ src->next_input_byte += num_bytes;
+ src->bytes_in_buffer -= num_bytes;
+ }
+}
+
+static void jpegr_term_source(j_decompress_ptr /*cinfo*/) {}
+
+jpegr_source_mgr::jpegr_source_mgr(const uint8_t* ptr, int len)
+ : mBufferPtr(ptr), mBufferLength(len) {
+ init_source = jpegr_init_source;
+ fill_input_buffer = jpegr_fill_input_buffer;
+ skip_input_data = jpegr_skip_input_data;
+ resync_to_restart = jpeg_resync_to_restart;
+ term_source = jpegr_term_source;
+}
+
+jpegr_source_mgr::~jpegr_source_mgr() {}
+
+static void jpegrerror_exit(j_common_ptr cinfo) {
+ jpegrerror_mgr* err = reinterpret_cast<jpegrerror_mgr*>(cinfo->err);
+ longjmp(err->setjmp_buffer, 1);
+}
+
+static void output_message(j_common_ptr cinfo) {
+ char buffer[JMSG_LENGTH_MAX];
+
+ /* Create the message */
+ (*cinfo->err->format_message)(cinfo, buffer);
+ ALOGE("%s\n", buffer);
+}
+
+JpegDecoderHelper::JpegDecoderHelper() {}
+
+JpegDecoderHelper::~JpegDecoderHelper() {}
+
+bool JpegDecoderHelper::decompressImage(const void* image, int length, bool decodeToRGBA) {
+ if (image == nullptr || length <= 0) {
+ ALOGE("Image size can not be handled: %d", length);
+ return false;
+ }
+ mResultBuffer.clear();
+ mXMPBuffer.clear();
+ return decode(image, length, decodeToRGBA);
+}
+
+void* JpegDecoderHelper::getDecompressedImagePtr() { return mResultBuffer.data(); }
+
+size_t JpegDecoderHelper::getDecompressedImageSize() { return mResultBuffer.size(); }
+
+void* JpegDecoderHelper::getXMPPtr() { return mXMPBuffer.data(); }
+
+size_t JpegDecoderHelper::getXMPSize() { return mXMPBuffer.size(); }
+
+void* JpegDecoderHelper::getEXIFPtr() { return mEXIFBuffer.data(); }
+
+size_t JpegDecoderHelper::getEXIFSize() { return mEXIFBuffer.size(); }
+
+void* JpegDecoderHelper::getICCPtr() { return mICCBuffer.data(); }
+
+size_t JpegDecoderHelper::getICCSize() { return mICCBuffer.size(); }
+
+size_t JpegDecoderHelper::getDecompressedImageWidth() { return mWidth; }
+
+size_t JpegDecoderHelper::getDecompressedImageHeight() { return mHeight; }
+
+// Here we only handle the first EXIF package, and in theary EXIF (or JFIF) must be the first
+// in the image file.
+// 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.
+bool JpegDecoderHelper::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;
+ myerr.pub.output_message = output_message;
+
+ 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);
+
+ cinfo.src = &mgr;
+ jpeg_read_header(&cinfo, TRUE);
+
+ size_t pos = 2; // position after SOI
+ for (jpeg_marker_struct* marker = cinfo.marker_list; marker; marker = marker->next) {
+ pos += 4;
+ pos += marker->original_length;
+
+ if (marker->marker != kAPP1Marker) {
+ continue;
+ }
+
+ const unsigned int len = marker->data_length;
+
+ if (len > sizeof(kExifIdCode) && !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
+ mEXIFBuffer.resize(len, 0);
+ memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
+ mExifPos = pos - marker->original_length;
+ break;
+ }
+ }
+
+ jpeg_destroy_decompress(&cinfo);
+ return true;
+}
+
+bool JpegDecoderHelper::decode(const void* image, int length, bool decodeToRGBA) {
+ bool status = true;
+ jpeg_decompress_struct cinfo;
+ jpegrerror_mgr myerr;
+ cinfo.err = jpeg_std_error(&myerr.pub);
+ myerr.pub.error_exit = jpegrerror_exit;
+ myerr.pub.output_message = output_message;
+
+ 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);
+
+ jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
+ cinfo.src = &mgr;
+ if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
+ jpeg_destroy_decompress(&cinfo);
+ return false;
+ }
+
+ // Save XMP data, EXIF data, and ICC data.
+ // Here we only handle the first XMP / EXIF / ICC package.
+ // 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.
+ bool exifAppears = false;
+ bool xmpAppears = false;
+ bool iccAppears = false;
+ size_t pos = 2; // position after SOI
+ for (jpeg_marker_struct* marker = cinfo.marker_list;
+ marker && !(exifAppears && xmpAppears && iccAppears); marker = marker->next) {
+ pos += 4;
+ pos += marker->original_length;
+ if (marker->marker != kAPP1Marker && marker->marker != kAPP2Marker) {
+ continue;
+ }
+ const unsigned int len = marker->data_length;
+ if (!xmpAppears && len > sizeof(kXmpNameSpace) &&
+ !memcmp(marker->data, kXmpNameSpace, sizeof(kXmpNameSpace))) {
+ mXMPBuffer.resize(len + 1, 0);
+ memcpy(static_cast<void*>(mXMPBuffer.data()), marker->data, len);
+ xmpAppears = true;
+ } else if (!exifAppears && len > sizeof(kExifIdCode) &&
+ !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
+ mEXIFBuffer.resize(len, 0);
+ memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
+ exifAppears = true;
+ mExifPos = pos - marker->original_length;
+ } else if (!iccAppears && len > sizeof(kICCSig) &&
+ !memcmp(marker->data, kICCSig, sizeof(kICCSig))) {
+ mICCBuffer.resize(len, 0);
+ memcpy(static_cast<void*>(mICCBuffer.data()), marker->data, len);
+ iccAppears = true;
+ }
+ }
+
+ mWidth = cinfo.image_width;
+ mHeight = cinfo.image_height;
+ if (mWidth > kMaxWidth || mHeight > kMaxHeight) {
+ status = false;
+ goto CleanUp;
+ }
+
+ if (decodeToRGBA) {
+ // The primary image is expected to be yuv420 sampling
+ if (cinfo.jpeg_color_space != JCS_YCbCr) {
+ status = false;
+ ALOGE("%s: decodeToRGBA unexpected jpeg color space ", __func__);
+ goto CleanUp;
+ }
+ if (cinfo.comp_info[0].h_samp_factor != 2 || cinfo.comp_info[0].v_samp_factor != 2 ||
+ cinfo.comp_info[1].h_samp_factor != 1 || cinfo.comp_info[1].v_samp_factor != 1 ||
+ cinfo.comp_info[2].h_samp_factor != 1 || cinfo.comp_info[2].v_samp_factor != 1) {
+ status = false;
+ ALOGE("%s: decodeToRGBA unexpected primary image sub-sampling", __func__);
+ goto CleanUp;
+ }
+ // 4 bytes per pixel
+ mResultBuffer.resize(cinfo.image_width * cinfo.image_height * 4);
+ cinfo.out_color_space = JCS_EXT_RGBA;
+ } else {
+ if (cinfo.jpeg_color_space == JCS_YCbCr) {
+ if (cinfo.comp_info[0].h_samp_factor != 2 || cinfo.comp_info[0].v_samp_factor != 2 ||
+ cinfo.comp_info[1].h_samp_factor != 1 || cinfo.comp_info[1].v_samp_factor != 1 ||
+ cinfo.comp_info[2].h_samp_factor != 1 || cinfo.comp_info[2].v_samp_factor != 1) {
+ status = false;
+ ALOGE("%s: decoding to YUV only supports 4:2:0 subsampling", __func__);
+ goto CleanUp;
+ }
+ mResultBuffer.resize(cinfo.image_width * cinfo.image_height * 3 / 2, 0);
+ } else if (cinfo.jpeg_color_space == JCS_GRAYSCALE) {
+ mResultBuffer.resize(cinfo.image_width * cinfo.image_height, 0);
+ } else {
+ status = false;
+ ALOGE("%s: decodeToYUV unexpected jpeg color space", __func__);
+ goto CleanUp;
+ }
+ cinfo.out_color_space = cinfo.jpeg_color_space;
+ cinfo.raw_data_out = TRUE;
+ }
+
+ cinfo.dct_method = JDCT_ISLOW;
+ jpeg_start_decompress(&cinfo);
+ if (!decompress(&cinfo, static_cast<const uint8_t*>(mResultBuffer.data()),
+ cinfo.jpeg_color_space == JCS_GRAYSCALE)) {
+ status = false;
+ goto CleanUp;
+ }
+
+CleanUp:
+ jpeg_finish_decompress(&cinfo);
+ jpeg_destroy_decompress(&cinfo);
+
+ return status;
+}
+
+bool JpegDecoderHelper::decompress(jpeg_decompress_struct* cinfo, const uint8_t* dest,
+ bool isSingleChannel) {
+ return isSingleChannel ? decompressSingleChannel(cinfo, dest)
+ : ((cinfo->out_color_space == JCS_EXT_RGBA) ? decompressRGBA(cinfo, dest)
+ : decompressYUV(cinfo, dest));
+}
+
+bool JpegDecoderHelper::getCompressedImageParameters(const void* image, int length, size_t* pWidth,
+ size_t* pHeight, std::vector<uint8_t>* iccData,
+ std::vector<uint8_t>* exifData) {
+ jpeg_decompress_struct cinfo;
+ jpegrerror_mgr myerr;
+ cinfo.err = jpeg_std_error(&myerr.pub);
+ myerr.pub.error_exit = jpegrerror_exit;
+ myerr.pub.output_message = output_message;
+
+ if (setjmp(myerr.setjmp_buffer)) {
+ jpeg_destroy_decompress(&cinfo);
+ return false;
+ }
+ jpeg_create_decompress(&cinfo);
+
+ jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
+ jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);
+
+ jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
+ cinfo.src = &mgr;
+ if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
+ jpeg_destroy_decompress(&cinfo);
+ return false;
+ }
+
+ if (pWidth != nullptr) {
+ *pWidth = cinfo.image_width;
+ }
+ if (pHeight != nullptr) {
+ *pHeight = cinfo.image_height;
+ }
+
+ if (iccData != nullptr) {
+ for (jpeg_marker_struct* marker = cinfo.marker_list; marker; marker = marker->next) {
+ if (marker->marker != kAPP2Marker) {
+ continue;
+ }
+ if (marker->data_length <= kICCMarkerHeaderSize ||
+ memcmp(marker->data, kICCSig, sizeof(kICCSig)) != 0) {
+ continue;
+ }
+
+ iccData->insert(iccData->end(), marker->data, marker->data + marker->data_length);
+ }
+ }
+
+ if (exifData != nullptr) {
+ bool exifAppears = false;
+ for (jpeg_marker_struct* marker = cinfo.marker_list; marker && !exifAppears;
+ marker = marker->next) {
+ if (marker->marker != kAPP1Marker) {
+ continue;
+ }
+
+ const unsigned int len = marker->data_length;
+ if (len >= sizeof(kExifIdCode) && !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
+ exifData->resize(len, 0);
+ memcpy(static_cast<void*>(exifData->data()), marker->data, len);
+ exifAppears = true;
+ }
+ }
+ }
+
+ jpeg_destroy_decompress(&cinfo);
+ return true;
+}
+
+bool JpegDecoderHelper::decompressRGBA(jpeg_decompress_struct* cinfo, const uint8_t* dest) {
+ JSAMPLE* out = (JSAMPLE*)dest;
+
+ while (cinfo->output_scanline < cinfo->image_height) {
+ if (1 != jpeg_read_scanlines(cinfo, &out, 1)) return false;
+ out += cinfo->image_width * 4;
+ }
+ return true;
+}
+
+bool JpegDecoderHelper::decompressYUV(jpeg_decompress_struct* cinfo, const uint8_t* dest) {
+ size_t luma_plane_size = cinfo->image_width * cinfo->image_height;
+ size_t chroma_plane_size = luma_plane_size / 4;
+ uint8_t* y_plane = const_cast<uint8_t*>(dest);
+ uint8_t* u_plane = const_cast<uint8_t*>(dest + luma_plane_size);
+ uint8_t* v_plane = const_cast<uint8_t*>(dest + luma_plane_size + chroma_plane_size);
+
+ const size_t aligned_width = ALIGNM(cinfo->image_width, kCompressBatchSize);
+ const bool is_width_aligned = (aligned_width == cinfo->image_width);
+ uint8_t* y_plane_intrm = nullptr;
+ uint8_t* u_plane_intrm = nullptr;
+ uint8_t* v_plane_intrm = nullptr;
+
+ JSAMPROW y[kCompressBatchSize];
+ JSAMPROW cb[kCompressBatchSize / 2];
+ JSAMPROW cr[kCompressBatchSize / 2];
+ JSAMPARRAY planes[3]{y, cb, cr};
+ JSAMPROW y_intrm[kCompressBatchSize];
+ JSAMPROW cb_intrm[kCompressBatchSize / 2];
+ JSAMPROW cr_intrm[kCompressBatchSize / 2];
+ JSAMPARRAY planes_intrm[3]{y_intrm, cb_intrm, cr_intrm};
+
+ if (cinfo->image_height % kCompressBatchSize != 0) {
+ mEmpty = std::make_unique<uint8_t[]>(aligned_width);
+ }
+
+ if (!is_width_aligned) {
+ size_t mcu_row_size = aligned_width * kCompressBatchSize * 3 / 2;
+ mBufferIntermediate = std::make_unique<uint8_t[]>(mcu_row_size);
+ y_plane_intrm = mBufferIntermediate.get();
+ u_plane_intrm = y_plane_intrm + (aligned_width * kCompressBatchSize);
+ v_plane_intrm = u_plane_intrm + (aligned_width * kCompressBatchSize) / 4;
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ y_intrm[i] = y_plane_intrm + i * aligned_width;
+ }
+ for (int i = 0; i < kCompressBatchSize / 2; ++i) {
+ int offset_intrm = i * (aligned_width / 2);
+ cb_intrm[i] = u_plane_intrm + offset_intrm;
+ cr_intrm[i] = v_plane_intrm + offset_intrm;
+ }
+ }
+
+ while (cinfo->output_scanline < cinfo->image_height) {
+ size_t scanline_copy = cinfo->output_scanline;
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ size_t scanline = cinfo->output_scanline + i;
+ if (scanline < cinfo->image_height) {
+ y[i] = y_plane + scanline * cinfo->image_width;
+ } else {
+ y[i] = mEmpty.get();
+ }
+ }
+ // cb, cr only have half scanlines
+ for (int i = 0; i < kCompressBatchSize / 2; ++i) {
+ size_t scanline = cinfo->output_scanline / 2 + i;
+ if (scanline < cinfo->image_height / 2) {
+ int offset = scanline * (cinfo->image_width / 2);
+ cb[i] = u_plane + offset;
+ cr[i] = v_plane + offset;
+ } else {
+ cb[i] = cr[i] = mEmpty.get();
+ }
+ }
+
+ int processed =
+ jpeg_read_raw_data(cinfo, is_width_aligned ? planes : planes_intrm, kCompressBatchSize);
+ if (processed != kCompressBatchSize) {
+ ALOGE("Number of processed lines does not equal input lines.");
+ return false;
+ }
+ if (!is_width_aligned) {
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ if (scanline_copy + i < cinfo->image_height) {
+ memcpy(y[i], y_intrm[i], cinfo->image_width);
+ }
+ }
+ for (int i = 0; i < kCompressBatchSize / 2; ++i) {
+ if (((scanline_copy / 2) + i) < (cinfo->image_height / 2)) {
+ memcpy(cb[i], cb_intrm[i], cinfo->image_width / 2);
+ memcpy(cr[i], cr_intrm[i], cinfo->image_width / 2);
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool JpegDecoderHelper::decompressSingleChannel(jpeg_decompress_struct* cinfo,
+ const uint8_t* dest) {
+ uint8_t* y_plane = const_cast<uint8_t*>(dest);
+ uint8_t* y_plane_intrm = nullptr;
+
+ const size_t aligned_width = ALIGNM(cinfo->image_width, kCompressBatchSize);
+ const bool is_width_aligned = (aligned_width == cinfo->image_width);
+
+ JSAMPROW y[kCompressBatchSize];
+ JSAMPARRAY planes[1]{y};
+ JSAMPROW y_intrm[kCompressBatchSize];
+ JSAMPARRAY planes_intrm[1]{y_intrm};
+
+ if (cinfo->image_height % kCompressBatchSize != 0) {
+ mEmpty = std::make_unique<uint8_t[]>(aligned_width);
+ }
+
+ if (!is_width_aligned) {
+ size_t mcu_row_size = aligned_width * kCompressBatchSize;
+ mBufferIntermediate = std::make_unique<uint8_t[]>(mcu_row_size);
+ y_plane_intrm = mBufferIntermediate.get();
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ y_intrm[i] = y_plane_intrm + i * aligned_width;
+ }
+ }
+
+ while (cinfo->output_scanline < cinfo->image_height) {
+ size_t scanline_copy = cinfo->output_scanline;
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ size_t scanline = cinfo->output_scanline + i;
+ if (scanline < cinfo->image_height) {
+ y[i] = y_plane + scanline * cinfo->image_width;
+ } else {
+ y[i] = mEmpty.get();
+ }
+ }
+
+ int processed =
+ jpeg_read_raw_data(cinfo, is_width_aligned ? planes : planes_intrm, kCompressBatchSize);
+ if (processed != kCompressBatchSize / 2) {
+ ALOGE("Number of processed lines does not equal input lines.");
+ return false;
+ }
+ if (!is_width_aligned) {
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ if (scanline_copy + i < cinfo->image_height) {
+ memcpy(y[i], y_intrm[i], cinfo->image_width);
+ }
+ }
+ }
+ }
+ return true;
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/jpegencoderhelper.cpp b/lib/src/jpegencoderhelper.cpp
new file mode 100644
index 0000000..7bab01f
--- /dev/null
+++ b/lib/src/jpegencoderhelper.cpp
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <cstring>
+#include <memory>
+#include <string>
+
+#include "ultrahdr/ultrahdrcommon.h"
+#include "ultrahdr/ultrahdr.h"
+#include "ultrahdr/jpegencoderhelper.h"
+
+namespace ultrahdr {
+
+// The destination manager that can access |mResultBuffer| in JpegEncoderHelper.
+struct destination_mgr {
+ struct jpeg_destination_mgr mgr;
+ JpegEncoderHelper* encoder;
+};
+
+JpegEncoderHelper::JpegEncoderHelper() {}
+
+JpegEncoderHelper::~JpegEncoderHelper() {}
+
+bool JpegEncoderHelper::compressImage(const uint8_t* yBuffer, const uint8_t* uvBuffer, int width,
+ int height, int lumaStride, int chromaStride, int quality,
+ const void* iccBuffer, unsigned int iccSize) {
+ mResultBuffer.clear();
+ if (!encode(yBuffer, uvBuffer, width, height, lumaStride, chromaStride, quality, iccBuffer,
+ iccSize)) {
+ return false;
+ }
+ ALOGV("Compressed JPEG: %d[%dx%d] -> %zu bytes", (width * height * 12) / 8, width, height,
+ mResultBuffer.size());
+ return true;
+}
+
+void* JpegEncoderHelper::getCompressedImagePtr() { return mResultBuffer.data(); }
+
+size_t JpegEncoderHelper::getCompressedImageSize() { return mResultBuffer.size(); }
+
+void JpegEncoderHelper::initDestination(j_compress_ptr cinfo) {
+ destination_mgr* dest = reinterpret_cast<destination_mgr*>(cinfo->dest);
+ std::vector<JOCTET>& buffer = dest->encoder->mResultBuffer;
+ buffer.resize(kBlockSize);
+ dest->mgr.next_output_byte = &buffer[0];
+ dest->mgr.free_in_buffer = buffer.size();
+}
+
+boolean JpegEncoderHelper::emptyOutputBuffer(j_compress_ptr cinfo) {
+ destination_mgr* dest = reinterpret_cast<destination_mgr*>(cinfo->dest);
+ std::vector<JOCTET>& buffer = dest->encoder->mResultBuffer;
+ size_t oldsize = buffer.size();
+ buffer.resize(oldsize + kBlockSize);
+ dest->mgr.next_output_byte = &buffer[oldsize];
+ dest->mgr.free_in_buffer = kBlockSize;
+ return true;
+}
+
+void JpegEncoderHelper::terminateDestination(j_compress_ptr cinfo) {
+ destination_mgr* dest = reinterpret_cast<destination_mgr*>(cinfo->dest);
+ std::vector<JOCTET>& buffer = dest->encoder->mResultBuffer;
+ buffer.resize(buffer.size() - dest->mgr.free_in_buffer);
+}
+
+void JpegEncoderHelper::outputErrorMessage(j_common_ptr cinfo) {
+ char buffer[JMSG_LENGTH_MAX];
+
+ /* Create the message */
+ (*cinfo->err->format_message)(cinfo, buffer);
+ ALOGE("%s\n", buffer);
+}
+
+bool JpegEncoderHelper::encode(const uint8_t* yBuffer, const uint8_t* uvBuffer, int width,
+ int height, int lumaStride, int chromaStride, int quality,
+ const void* iccBuffer, unsigned int iccSize) {
+ jpeg_compress_struct cinfo;
+ jpeg_error_mgr jerr;
+
+ cinfo.err = jpeg_std_error(&jerr);
+ cinfo.err->output_message = &outputErrorMessage;
+ jpeg_create_compress(&cinfo);
+ setJpegDestination(&cinfo);
+ setJpegCompressStruct(width, height, quality, &cinfo, uvBuffer == nullptr);
+ jpeg_start_compress(&cinfo, TRUE);
+ if (iccBuffer != nullptr && iccSize > 0) {
+ jpeg_write_marker(&cinfo, JPEG_APP0 + 2, static_cast<const JOCTET*>(iccBuffer), iccSize);
+ }
+ bool status = cinfo.num_components == 1
+ ? compressY(&cinfo, yBuffer, lumaStride)
+ : compressYuv(&cinfo, yBuffer, uvBuffer, lumaStride, chromaStride);
+ jpeg_finish_compress(&cinfo);
+ jpeg_destroy_compress(&cinfo);
+
+ return status;
+}
+
+void JpegEncoderHelper::setJpegDestination(jpeg_compress_struct* cinfo) {
+ destination_mgr* dest = static_cast<struct destination_mgr*>(
+ (*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_PERMANENT, sizeof(destination_mgr)));
+ dest->encoder = this;
+ dest->mgr.init_destination = &initDestination;
+ dest->mgr.empty_output_buffer = &emptyOutputBuffer;
+ dest->mgr.term_destination = &terminateDestination;
+ cinfo->dest = reinterpret_cast<struct jpeg_destination_mgr*>(dest);
+}
+
+void JpegEncoderHelper::setJpegCompressStruct(int width, int height, int quality,
+ jpeg_compress_struct* cinfo, bool isSingleChannel) {
+ cinfo->image_width = width;
+ cinfo->image_height = height;
+ cinfo->input_components = isSingleChannel ? 1 : 3;
+ cinfo->in_color_space = isSingleChannel ? JCS_GRAYSCALE : JCS_YCbCr;
+ jpeg_set_defaults(cinfo);
+ jpeg_set_quality(cinfo, quality, TRUE);
+ cinfo->raw_data_in = TRUE;
+ cinfo->dct_method = JDCT_ISLOW;
+ cinfo->comp_info[0].h_samp_factor = cinfo->in_color_space == JCS_GRAYSCALE ? 1 : 2;
+ cinfo->comp_info[0].v_samp_factor = cinfo->in_color_space == JCS_GRAYSCALE ? 1 : 2;
+ for (int i = 1; i < cinfo->num_components; i++) {
+ cinfo->comp_info[i].h_samp_factor = 1;
+ cinfo->comp_info[i].v_samp_factor = 1;
+ }
+}
+
+bool JpegEncoderHelper::compressYuv(jpeg_compress_struct* cinfo, const uint8_t* yBuffer,
+ const uint8_t* uvBuffer, int lumaStride, int chromaStride) {
+ size_t chroma_plane_size = chromaStride * cinfo->image_height / 2;
+ uint8_t* y_plane = const_cast<uint8_t*>(yBuffer);
+ uint8_t* u_plane = const_cast<uint8_t*>(uvBuffer);
+ uint8_t* v_plane = const_cast<uint8_t*>(u_plane + chroma_plane_size);
+
+ const int aligned_width = ALIGNM(cinfo->image_width, kCompressBatchSize);
+ const bool need_luma_padding = (lumaStride < aligned_width);
+ const int aligned_chroma_width = ALIGNM(cinfo->image_width / 2, kCompressBatchSize / 2);
+ const bool need_chroma_padding = (chromaStride < aligned_chroma_width);
+
+ std::unique_ptr<uint8_t[]> empty = nullptr;
+ std::unique_ptr<uint8_t[]> y_mcu_row = nullptr;
+ std::unique_ptr<uint8_t[]> cb_mcu_row = nullptr;
+ std::unique_ptr<uint8_t[]> cr_mcu_row = nullptr;
+ uint8_t* y_mcu_row_ptr = nullptr;
+ uint8_t* cb_mcu_row_ptr = nullptr;
+ uint8_t* cr_mcu_row_ptr = nullptr;
+
+ JSAMPROW y[kCompressBatchSize];
+ JSAMPROW cb[kCompressBatchSize / 2];
+ JSAMPROW cr[kCompressBatchSize / 2];
+ JSAMPARRAY planes[3]{y, cb, cr};
+
+ if (cinfo->image_height % kCompressBatchSize != 0) {
+ empty = std::make_unique<uint8_t[]>(aligned_width);
+ memset(empty.get(), 0, aligned_width);
+ }
+
+ if (need_luma_padding) {
+ size_t mcu_row_size = aligned_width * kCompressBatchSize;
+ y_mcu_row = std::make_unique<uint8_t[]>(mcu_row_size);
+ y_mcu_row_ptr = y_mcu_row.get();
+ uint8_t* tmp = y_mcu_row_ptr;
+ for (int i = 0; i < kCompressBatchSize; ++i, tmp += aligned_width) {
+ memset(tmp + cinfo->image_width, 0, aligned_width - cinfo->image_width);
+ }
+ }
+
+ if (need_chroma_padding) {
+ size_t mcu_row_size = aligned_chroma_width * kCompressBatchSize / 2;
+ cb_mcu_row = std::make_unique<uint8_t[]>(mcu_row_size);
+ cb_mcu_row_ptr = cb_mcu_row.get();
+ cr_mcu_row = std::make_unique<uint8_t[]>(mcu_row_size);
+ cr_mcu_row_ptr = cr_mcu_row.get();
+ uint8_t* tmp1 = cb_mcu_row_ptr;
+ uint8_t* tmp2 = cr_mcu_row_ptr;
+ for (int i = 0; i < kCompressBatchSize / 2;
+ ++i, tmp1 += aligned_chroma_width, tmp2 += aligned_chroma_width) {
+ memset(tmp1 + cinfo->image_width / 2, 0, aligned_chroma_width - (cinfo->image_width / 2));
+ memset(tmp2 + cinfo->image_width / 2, 0, aligned_chroma_width - (cinfo->image_width / 2));
+ }
+ }
+
+ while (cinfo->next_scanline < cinfo->image_height) {
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ size_t scanline = cinfo->next_scanline + i;
+ if (scanline < cinfo->image_height) {
+ y[i] = y_plane + scanline * lumaStride;
+ if (need_luma_padding) {
+ uint8_t* tmp = y_mcu_row_ptr + i * aligned_width;
+ memcpy(tmp, y[i], cinfo->image_width);
+ y[i] = tmp;
+ }
+ } else {
+ y[i] = empty.get();
+ }
+ }
+ // cb, cr only have half scanlines
+ for (int i = 0; i < kCompressBatchSize / 2; ++i) {
+ size_t scanline = cinfo->next_scanline / 2 + i;
+ if (scanline < cinfo->image_height / 2) {
+ int offset = scanline * chromaStride;
+ cb[i] = u_plane + offset;
+ cr[i] = v_plane + offset;
+ if (need_chroma_padding) {
+ uint8_t* tmp = cb_mcu_row_ptr + i * aligned_chroma_width;
+ memcpy(tmp, cb[i], cinfo->image_width / 2);
+ cb[i] = tmp;
+ tmp = cr_mcu_row_ptr + i * aligned_chroma_width;
+ memcpy(tmp, cr[i], cinfo->image_width / 2);
+ cr[i] = tmp;
+ }
+ } else {
+ cb[i] = cr[i] = empty.get();
+ }
+ }
+ int processed = jpeg_write_raw_data(cinfo, planes, kCompressBatchSize);
+ if (processed != kCompressBatchSize) {
+ ALOGE("Number of processed lines does not equal input lines.");
+ return false;
+ }
+ }
+ return true;
+}
+
+bool JpegEncoderHelper::compressY(jpeg_compress_struct* cinfo, const uint8_t* yBuffer,
+ int lumaStride) {
+ uint8_t* y_plane = const_cast<uint8_t*>(yBuffer);
+
+ const int aligned_luma_width = ALIGNM(cinfo->image_width, kCompressBatchSize);
+ const bool need_luma_padding = (lumaStride < aligned_luma_width);
+
+ std::unique_ptr<uint8_t[]> empty = nullptr;
+ std::unique_ptr<uint8_t[]> y_mcu_row = nullptr;
+ uint8_t* y_mcu_row_ptr = nullptr;
+
+ JSAMPROW y[kCompressBatchSize];
+ JSAMPARRAY planes[1]{y};
+
+ if (cinfo->image_height % kCompressBatchSize != 0) {
+ empty = std::make_unique<uint8_t[]>(aligned_luma_width);
+ memset(empty.get(), 0, aligned_luma_width);
+ }
+
+ if (need_luma_padding) {
+ size_t mcu_row_size = aligned_luma_width * kCompressBatchSize;
+ y_mcu_row = std::make_unique<uint8_t[]>(mcu_row_size);
+ y_mcu_row_ptr = y_mcu_row.get();
+ uint8_t* tmp = y_mcu_row_ptr;
+ for (int i = 0; i < kCompressBatchSize; ++i, tmp += aligned_luma_width) {
+ memset(tmp + cinfo->image_width, 0, aligned_luma_width - cinfo->image_width);
+ }
+ }
+
+ while (cinfo->next_scanline < cinfo->image_height) {
+ for (int i = 0; i < kCompressBatchSize; ++i) {
+ size_t scanline = cinfo->next_scanline + i;
+ if (scanline < cinfo->image_height) {
+ y[i] = y_plane + scanline * lumaStride;
+ if (need_luma_padding) {
+ uint8_t* tmp = y_mcu_row_ptr + i * aligned_luma_width;
+ memcpy(tmp, y[i], cinfo->image_width);
+ y[i] = tmp;
+ }
+ } else {
+ y[i] = empty.get();
+ }
+ }
+ int processed = jpeg_write_raw_data(cinfo, planes, kCompressBatchSize);
+ if (processed != kCompressBatchSize / 2) {
+ ALOGE("Number of processed lines does not equal input lines.");
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/jpegr.cpp b/lib/src/jpegr.cpp
new file mode 100644
index 0000000..ed4fef1
--- /dev/null
+++ b/lib/src/jpegr.cpp
@@ -0,0 +1,1509 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <sysinfoapi.h>
+#else
+#include <unistd.h>
+#endif
+
+#include <condition_variable>
+#include <deque>
+#include <functional>
+#include <mutex>
+#include <thread>
+
+#include "ultrahdr/ultrahdrcommon.h"
+#include "ultrahdr/jpegr.h"
+#include "ultrahdr/icc.h"
+#include "ultrahdr/multipictureformat.h"
+
+#include "image_io/base/data_segment_data_source.h"
+#include "image_io/jpeg/jpeg_info.h"
+#include "image_io/jpeg/jpeg_info_builder.h"
+#include "image_io/jpeg/jpeg_marker.h"
+#include "image_io/jpeg/jpeg_scanner.h"
+
+using namespace std;
+using namespace photos_editing_formats::image_io;
+
+namespace ultrahdr {
+
+#define USE_SRGB_INVOETF_LUT 1
+#define USE_HLG_OETF_LUT 1
+#define USE_PQ_OETF_LUT 1
+#define USE_HLG_INVOETF_LUT 1
+#define USE_PQ_INVOETF_LUT 1
+#define USE_APPLY_GAIN_LUT 1
+
+#define JPEGR_CHECK(x) \
+ { \
+ status_t status = (x); \
+ if ((status) != JPEGR_NO_ERROR) { \
+ return status; \
+ } \
+ }
+
+// JPEG compress quality (0 ~ 100) for gain map
+static const int kMapCompressQuality = 85;
+
+int GetCPUCoreCount() {
+ int cpuCoreCount = 1;
+
+#if defined(_WIN32)
+ SYSTEM_INFO system_info;
+ ZeroMemory(&system_info, sizeof(system_info));
+ GetSystemInfo(&system_info);
+ cpuCoreCount = (size_t)system_info.dwNumberOfProcessors;
+#elif defined(_SC_NPROCESSORS_ONLN)
+ cpuCoreCount = sysconf(_SC_NPROCESSORS_ONLN);
+#elif defined(_SC_NPROCESSORS_CONF)
+ cpuCoreCount = sysconf(_SC_NPROCESSORS_CONF);
+#else
+#error platform-specific implementation for GetCPUCoreCount() missing.
+#endif
+ if (cpuCoreCount <= 0) cpuCoreCount = 1;
+ return cpuCoreCount;
+}
+
+/*
+ * MessageWriter implementation for ALOG functions.
+ */
+class AlogMessageWriter : public MessageWriter {
+ public:
+ void WriteMessage(const Message& message) override {
+ std::string log = GetFormattedMessage(message);
+ ALOGD("%s", log.c_str());
+ }
+};
+
+/*
+ * Helper function copies the JPEG image from without EXIF.
+ *
+ * @param pDest destination of the data to be written.
+ * @param pSource source of data being written.
+ * @param exif_pos position of the EXIF package, which is aligned with jpegdecoder.getEXIFPos().
+ * (4 bytes offset to FF sign, the byte after FF E1 XX XX <this byte>).
+ * @param exif_size exif size without the initial 4 bytes, aligned with jpegdecoder.getEXIFSize().
+ */
+static void copyJpegWithoutExif(jr_compressed_ptr pDest, jr_compressed_ptr pSource, size_t exif_pos,
+ size_t exif_size) {
+ const size_t exif_offset = 4; // exif_pos has 4 bytes offset to the FF sign
+ pDest->length = pSource->length - exif_size - exif_offset;
+ pDest->data = new uint8_t[pDest->length];
+ pDest->maxLength = pDest->length;
+ pDest->colorGamut = pSource->colorGamut;
+ memcpy(pDest->data, pSource->data, exif_pos - exif_offset);
+ memcpy((uint8_t*)pDest->data + exif_pos - exif_offset,
+ (uint8_t*)pSource->data + exif_pos + exif_size, pSource->length - exif_pos - exif_size);
+}
+
+status_t JpegR::areInputArgumentsValid(jr_uncompressed_ptr p010_image_ptr,
+ jr_uncompressed_ptr yuv420_image_ptr,
+ ultrahdr_transfer_function hdr_tf,
+ jr_compressed_ptr dest_ptr) {
+ if (p010_image_ptr == nullptr || p010_image_ptr->data == nullptr) {
+ ALOGE("Received nullptr for input p010 image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (p010_image_ptr->width % 2 != 0 || p010_image_ptr->height % 2 != 0) {
+ ALOGE("Image dimensions cannot be odd, image dimensions %zux%zu", p010_image_ptr->width,
+ p010_image_ptr->height);
+ return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT;
+ }
+ if (p010_image_ptr->width < kMinWidth || p010_image_ptr->height < kMinHeight) {
+ ALOGE("Image dimensions cannot be less than %dx%d, image dimensions %zux%zu", kMinWidth,
+ kMinHeight, p010_image_ptr->width, p010_image_ptr->height);
+ return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT;
+ }
+ if (p010_image_ptr->width > kMaxWidth || p010_image_ptr->height > kMaxHeight) {
+ ALOGE("Image dimensions cannot be larger than %dx%d, image dimensions %zux%zu", kMaxWidth,
+ kMaxHeight, p010_image_ptr->width, p010_image_ptr->height);
+ return ERROR_JPEGR_UNSUPPORTED_WIDTH_HEIGHT;
+ }
+ if (p010_image_ptr->colorGamut <= ULTRAHDR_COLORGAMUT_UNSPECIFIED ||
+ p010_image_ptr->colorGamut > ULTRAHDR_COLORGAMUT_MAX) {
+ ALOGE("Unrecognized p010 color gamut %d", p010_image_ptr->colorGamut);
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ if (p010_image_ptr->luma_stride != 0 && p010_image_ptr->luma_stride < p010_image_ptr->width) {
+ ALOGE("Luma stride must not be smaller than width, stride=%zu, width=%zu",
+ p010_image_ptr->luma_stride, p010_image_ptr->width);
+ return ERROR_JPEGR_INVALID_STRIDE;
+ }
+ if (p010_image_ptr->chroma_data != nullptr &&
+ p010_image_ptr->chroma_stride < p010_image_ptr->width) {
+ ALOGE("Chroma stride must not be smaller than width, stride=%zu, width=%zu",
+ p010_image_ptr->chroma_stride, p010_image_ptr->width);
+ return ERROR_JPEGR_INVALID_STRIDE;
+ }
+ if (dest_ptr == nullptr || dest_ptr->data == nullptr) {
+ ALOGE("Received nullptr for destination");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (hdr_tf <= ULTRAHDR_TF_UNSPECIFIED || hdr_tf > ULTRAHDR_TF_MAX || hdr_tf == ULTRAHDR_TF_SRGB) {
+ ALOGE("Invalid hdr transfer function %d", hdr_tf);
+ return ERROR_JPEGR_INVALID_TRANS_FUNC;
+ }
+ if (yuv420_image_ptr == nullptr) {
+ return JPEGR_NO_ERROR;
+ }
+ if (yuv420_image_ptr->data == nullptr) {
+ ALOGE("Received nullptr for uncompressed 420 image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (yuv420_image_ptr->luma_stride != 0 &&
+ yuv420_image_ptr->luma_stride < yuv420_image_ptr->width) {
+ ALOGE("Luma stride must not be smaller than width, stride=%zu, width=%zu",
+ yuv420_image_ptr->luma_stride, yuv420_image_ptr->width);
+ return ERROR_JPEGR_INVALID_STRIDE;
+ }
+ if (yuv420_image_ptr->chroma_data != nullptr &&
+ yuv420_image_ptr->chroma_stride < yuv420_image_ptr->width / 2) {
+ ALOGE("Chroma stride must not be smaller than (width / 2), stride=%zu, width=%zu",
+ yuv420_image_ptr->chroma_stride, yuv420_image_ptr->width);
+ return ERROR_JPEGR_INVALID_STRIDE;
+ }
+ if (p010_image_ptr->width != yuv420_image_ptr->width ||
+ p010_image_ptr->height != yuv420_image_ptr->height) {
+ ALOGE("Image resolutions mismatch: P010: %zux%zu, YUV420: %zux%zu", p010_image_ptr->width,
+ p010_image_ptr->height, yuv420_image_ptr->width, yuv420_image_ptr->height);
+ return ERROR_JPEGR_RESOLUTION_MISMATCH;
+ }
+ if (yuv420_image_ptr->colorGamut <= ULTRAHDR_COLORGAMUT_UNSPECIFIED ||
+ yuv420_image_ptr->colorGamut > ULTRAHDR_COLORGAMUT_MAX) {
+ ALOGE("Unrecognized 420 color gamut %d", yuv420_image_ptr->colorGamut);
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::areInputArgumentsValid(jr_uncompressed_ptr p010_image_ptr,
+ jr_uncompressed_ptr yuv420_image_ptr,
+ ultrahdr_transfer_function hdr_tf,
+ jr_compressed_ptr dest_ptr, int quality) {
+ if (quality < 0 || quality > 100) {
+ ALOGE("quality factor is out side range [0-100], quality factor : %d", quality);
+ return ERROR_JPEGR_INVALID_QUALITY_FACTOR;
+ }
+ return areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest_ptr);
+}
+
+/* Encode API-0 */
+status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr, ultrahdr_transfer_function hdr_tf,
+ jr_compressed_ptr dest, int quality, jr_exif_ptr exif) {
+ // validate input arguments
+ JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, nullptr, hdr_tf, dest, quality));
+ if (exif != nullptr && exif->data == nullptr) {
+ ALOGE("received nullptr for exif metadata");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+
+ // clean up input structure for later usage
+ jpegr_uncompressed_struct p010_image = *p010_image_ptr;
+ if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width;
+ if (!p010_image.chroma_data) {
+ uint16_t* data = reinterpret_cast<uint16_t*>(p010_image.data);
+ p010_image.chroma_data = data + p010_image.luma_stride * p010_image.height;
+ p010_image.chroma_stride = p010_image.luma_stride;
+ }
+
+ const size_t yu420_luma_stride = ALIGNM(p010_image.width, JpegEncoderHelper::kCompressBatchSize);
+ unique_ptr<uint8_t[]> yuv420_image_data =
+ make_unique<uint8_t[]>(yu420_luma_stride * p010_image.height * 3 / 2);
+ jpegr_uncompressed_struct yuv420_image;
+ yuv420_image.data = yuv420_image_data.get();
+ yuv420_image.width = p010_image.width;
+ yuv420_image.height = p010_image.height;
+ yuv420_image.colorGamut = p010_image.colorGamut;
+ yuv420_image.chroma_data = nullptr;
+ yuv420_image.luma_stride = yu420_luma_stride;
+ yuv420_image.chroma_stride = yu420_luma_stride >> 1;
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ yuv420_image.chroma_data = data + yuv420_image.luma_stride * yuv420_image.height;
+
+ // tone map
+ JPEGR_CHECK(toneMap(&p010_image, &yuv420_image));
+
+ // gain map
+ ultrahdr_metadata_struct metadata;
+ metadata.version = kJpegrVersion;
+ jpegr_uncompressed_struct gainmap_image;
+ JPEGR_CHECK(generateGainMap(&yuv420_image, &p010_image, hdr_tf, &metadata, &gainmap_image));
+ std::unique_ptr<uint8_t[]> map_data;
+ map_data.reset(reinterpret_cast<uint8_t*>(gainmap_image.data));
+
+ // compress gain map
+ JpegEncoderHelper jpeg_enc_obj_gm;
+ JPEGR_CHECK(compressGainMap(&gainmap_image, &jpeg_enc_obj_gm));
+ jpegr_compressed_struct compressed_map;
+ compressed_map.data = jpeg_enc_obj_gm.getCompressedImagePtr();
+ compressed_map.length = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ compressed_map.maxLength = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+
+ std::shared_ptr<DataStruct> icc =
+ IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB, yuv420_image.colorGamut);
+
+ // convert to Bt601 YUV encoding for JPEG encode
+ if (yuv420_image.colorGamut != ULTRAHDR_COLORGAMUT_P3) {
+ JPEGR_CHECK(convertYuv(&yuv420_image, yuv420_image.colorGamut, ULTRAHDR_COLORGAMUT_P3));
+ }
+
+ // compress 420 image
+ JpegEncoderHelper jpeg_enc_obj_yuv420;
+ if (!jpeg_enc_obj_yuv420.compressImage(reinterpret_cast<uint8_t*>(yuv420_image.data),
+ reinterpret_cast<uint8_t*>(yuv420_image.chroma_data),
+ yuv420_image.width, yuv420_image.height,
+ yuv420_image.luma_stride, yuv420_image.chroma_stride,
+ quality, icc->getData(), icc->getLength())) {
+ return ERROR_JPEGR_ENCODE_ERROR;
+ }
+ jpegr_compressed_struct jpeg;
+ jpeg.data = jpeg_enc_obj_yuv420.getCompressedImagePtr();
+ jpeg.length = static_cast<int>(jpeg_enc_obj_yuv420.getCompressedImageSize());
+ jpeg.maxLength = static_cast<int>(jpeg_enc_obj_yuv420.getCompressedImageSize());
+ jpeg.colorGamut = yuv420_image.colorGamut;
+
+ // append gain map, no ICC since JPEG encode already did it
+ JPEGR_CHECK(appendGainMap(&jpeg, &compressed_map, exif, /* icc */ nullptr, /* icc size */ 0,
+ &metadata, dest));
+
+ return JPEGR_NO_ERROR;
+}
+
+/* Encode API-1 */
+status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr,
+ jr_uncompressed_ptr yuv420_image_ptr, ultrahdr_transfer_function hdr_tf,
+ jr_compressed_ptr dest, int quality, jr_exif_ptr exif) {
+ // validate input arguments
+ if (yuv420_image_ptr == nullptr) {
+ ALOGE("received nullptr for uncompressed 420 image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (exif != nullptr && exif->data == nullptr) {
+ ALOGE("received nullptr for exif metadata");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest, quality))
+
+ // clean up input structure for later usage
+ jpegr_uncompressed_struct p010_image = *p010_image_ptr;
+ if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width;
+ if (!p010_image.chroma_data) {
+ uint16_t* data = reinterpret_cast<uint16_t*>(p010_image.data);
+ p010_image.chroma_data = data + p010_image.luma_stride * p010_image.height;
+ p010_image.chroma_stride = p010_image.luma_stride;
+ }
+ jpegr_uncompressed_struct yuv420_image = *yuv420_image_ptr;
+ if (yuv420_image.luma_stride == 0) yuv420_image.luma_stride = yuv420_image.width;
+ if (!yuv420_image.chroma_data) {
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ yuv420_image.chroma_data = data + yuv420_image.luma_stride * yuv420_image.height;
+ yuv420_image.chroma_stride = yuv420_image.luma_stride >> 1;
+ }
+
+ // gain map
+ ultrahdr_metadata_struct metadata;
+ metadata.version = kJpegrVersion;
+ jpegr_uncompressed_struct gainmap_image;
+ JPEGR_CHECK(generateGainMap(&yuv420_image, &p010_image, hdr_tf, &metadata, &gainmap_image));
+ std::unique_ptr<uint8_t[]> map_data;
+ map_data.reset(reinterpret_cast<uint8_t*>(gainmap_image.data));
+
+ // compress gain map
+ JpegEncoderHelper jpeg_enc_obj_gm;
+ JPEGR_CHECK(compressGainMap(&gainmap_image, &jpeg_enc_obj_gm));
+ jpegr_compressed_struct compressed_map;
+ compressed_map.data = jpeg_enc_obj_gm.getCompressedImagePtr();
+ compressed_map.length = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ compressed_map.maxLength = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ compressed_map.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+
+ std::shared_ptr<DataStruct> icc =
+ IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB, yuv420_image.colorGamut);
+
+ jpegr_uncompressed_struct yuv420_bt601_image = yuv420_image;
+ unique_ptr<uint8_t[]> yuv_420_bt601_data;
+ // Convert to bt601 YUV encoding for JPEG encode
+ if (yuv420_image.colorGamut != ULTRAHDR_COLORGAMUT_P3) {
+ const size_t yuv_420_bt601_luma_stride =
+ ALIGNM(yuv420_image.width, JpegEncoderHelper::kCompressBatchSize);
+ yuv_420_bt601_data =
+ make_unique<uint8_t[]>(yuv_420_bt601_luma_stride * yuv420_image.height * 3 / 2);
+ yuv420_bt601_image.data = yuv_420_bt601_data.get();
+ yuv420_bt601_image.colorGamut = yuv420_image.colorGamut;
+ yuv420_bt601_image.luma_stride = yuv_420_bt601_luma_stride;
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_bt601_image.data);
+ yuv420_bt601_image.chroma_data = data + yuv_420_bt601_luma_stride * yuv420_image.height;
+ yuv420_bt601_image.chroma_stride = yuv_420_bt601_luma_stride >> 1;
+
+ {
+ // copy luma
+ uint8_t* y_dst = reinterpret_cast<uint8_t*>(yuv420_bt601_image.data);
+ uint8_t* y_src = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ if (yuv420_bt601_image.luma_stride == yuv420_image.luma_stride) {
+ memcpy(y_dst, y_src, yuv420_bt601_image.luma_stride * yuv420_image.height);
+ } else {
+ for (size_t i = 0; i < yuv420_image.height; i++) {
+ memcpy(y_dst, y_src, yuv420_image.width);
+ if (yuv420_image.width != yuv420_bt601_image.luma_stride) {
+ memset(y_dst + yuv420_image.width, 0,
+ yuv420_bt601_image.luma_stride - yuv420_image.width);
+ }
+ y_dst += yuv420_bt601_image.luma_stride;
+ y_src += yuv420_image.luma_stride;
+ }
+ }
+ }
+
+ if (yuv420_bt601_image.chroma_stride == yuv420_image.chroma_stride) {
+ // copy luma
+ uint8_t* ch_dst = reinterpret_cast<uint8_t*>(yuv420_bt601_image.chroma_data);
+ uint8_t* ch_src = reinterpret_cast<uint8_t*>(yuv420_image.chroma_data);
+ memcpy(ch_dst, ch_src, yuv420_bt601_image.chroma_stride * yuv420_image.height);
+ } else {
+ // copy cb & cr
+ uint8_t* cb_dst = reinterpret_cast<uint8_t*>(yuv420_bt601_image.chroma_data);
+ uint8_t* cb_src = reinterpret_cast<uint8_t*>(yuv420_image.chroma_data);
+ uint8_t* cr_dst = cb_dst + (yuv420_bt601_image.chroma_stride * yuv420_bt601_image.height / 2);
+ uint8_t* cr_src = cb_src + (yuv420_image.chroma_stride * yuv420_image.height / 2);
+ for (size_t i = 0; i < yuv420_image.height / 2; i++) {
+ memcpy(cb_dst, cb_src, yuv420_image.width / 2);
+ memcpy(cr_dst, cr_src, yuv420_image.width / 2);
+ if (yuv420_bt601_image.width / 2 != yuv420_bt601_image.chroma_stride) {
+ memset(cb_dst + yuv420_image.width / 2, 0,
+ yuv420_bt601_image.chroma_stride - yuv420_image.width / 2);
+ memset(cr_dst + yuv420_image.width / 2, 0,
+ yuv420_bt601_image.chroma_stride - yuv420_image.width / 2);
+ }
+ cb_dst += yuv420_bt601_image.chroma_stride;
+ cb_src += yuv420_image.chroma_stride;
+ cr_dst += yuv420_bt601_image.chroma_stride;
+ cr_src += yuv420_image.chroma_stride;
+ }
+ }
+ JPEGR_CHECK(convertYuv(&yuv420_bt601_image, yuv420_image.colorGamut, ULTRAHDR_COLORGAMUT_P3));
+ }
+
+ // compress 420 image
+ JpegEncoderHelper jpeg_enc_obj_yuv420;
+ if (!jpeg_enc_obj_yuv420.compressImage(
+ reinterpret_cast<uint8_t*>(yuv420_bt601_image.data),
+ reinterpret_cast<uint8_t*>(yuv420_bt601_image.chroma_data), yuv420_bt601_image.width,
+ yuv420_bt601_image.height, yuv420_bt601_image.luma_stride,
+ yuv420_bt601_image.chroma_stride, quality, icc->getData(), icc->getLength())) {
+ return ERROR_JPEGR_ENCODE_ERROR;
+ }
+
+ jpegr_compressed_struct jpeg;
+ jpeg.data = jpeg_enc_obj_yuv420.getCompressedImagePtr();
+ jpeg.length = static_cast<int>(jpeg_enc_obj_yuv420.getCompressedImageSize());
+ jpeg.maxLength = static_cast<int>(jpeg_enc_obj_yuv420.getCompressedImageSize());
+ jpeg.colorGamut = yuv420_image.colorGamut;
+
+ // append gain map, no ICC since JPEG encode already did it
+ JPEGR_CHECK(appendGainMap(&jpeg, &compressed_map, exif, /* icc */ nullptr, /* icc size */ 0,
+ &metadata, dest));
+ return JPEGR_NO_ERROR;
+}
+
+/* Encode API-2 */
+status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr,
+ jr_uncompressed_ptr yuv420_image_ptr,
+ jr_compressed_ptr yuv420jpg_image_ptr,
+ ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest) {
+ // validate input arguments
+ if (yuv420_image_ptr == nullptr) {
+ ALOGE("received nullptr for uncompressed 420 image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed jpeg image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, yuv420_image_ptr, hdr_tf, dest))
+
+ // clean up input structure for later usage
+ jpegr_uncompressed_struct p010_image = *p010_image_ptr;
+ if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width;
+ if (!p010_image.chroma_data) {
+ uint16_t* data = reinterpret_cast<uint16_t*>(p010_image.data);
+ p010_image.chroma_data = data + p010_image.luma_stride * p010_image.height;
+ p010_image.chroma_stride = p010_image.luma_stride;
+ }
+ jpegr_uncompressed_struct yuv420_image = *yuv420_image_ptr;
+ if (yuv420_image.luma_stride == 0) yuv420_image.luma_stride = yuv420_image.width;
+ if (!yuv420_image.chroma_data) {
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ yuv420_image.chroma_data = data + yuv420_image.luma_stride * p010_image.height;
+ yuv420_image.chroma_stride = yuv420_image.luma_stride >> 1;
+ }
+
+ // gain map
+ ultrahdr_metadata_struct metadata;
+ metadata.version = kJpegrVersion;
+ jpegr_uncompressed_struct gainmap_image;
+ JPEGR_CHECK(generateGainMap(&yuv420_image, &p010_image, hdr_tf, &metadata, &gainmap_image));
+ std::unique_ptr<uint8_t[]> map_data;
+ map_data.reset(reinterpret_cast<uint8_t*>(gainmap_image.data));
+
+ // compress gain map
+ JpegEncoderHelper jpeg_enc_obj_gm;
+ JPEGR_CHECK(compressGainMap(&gainmap_image, &jpeg_enc_obj_gm));
+ jpegr_compressed_struct gainmapjpg_image;
+ gainmapjpg_image.data = jpeg_enc_obj_gm.getCompressedImagePtr();
+ gainmapjpg_image.length = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ gainmapjpg_image.maxLength = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ gainmapjpg_image.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+
+ return encodeJPEGR(yuv420jpg_image_ptr, &gainmapjpg_image, &metadata, dest);
+}
+
+/* Encode API-3 */
+status_t JpegR::encodeJPEGR(jr_uncompressed_ptr p010_image_ptr,
+ jr_compressed_ptr yuv420jpg_image_ptr,
+ ultrahdr_transfer_function hdr_tf, jr_compressed_ptr dest) {
+ // validate input arguments
+ if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed jpeg image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ JPEGR_CHECK(areInputArgumentsValid(p010_image_ptr, nullptr, hdr_tf, dest))
+
+ // clean up input structure for later usage
+ jpegr_uncompressed_struct p010_image = *p010_image_ptr;
+ if (p010_image.luma_stride == 0) p010_image.luma_stride = p010_image.width;
+ if (!p010_image.chroma_data) {
+ uint16_t* data = reinterpret_cast<uint16_t*>(p010_image.data);
+ p010_image.chroma_data = data + p010_image.luma_stride * p010_image.height;
+ p010_image.chroma_stride = p010_image.luma_stride;
+ }
+
+ // decode input jpeg, gamut is going to be bt601.
+ JpegDecoderHelper jpeg_dec_obj_yuv420;
+ if (!jpeg_dec_obj_yuv420.decompressImage(yuv420jpg_image_ptr->data,
+ yuv420jpg_image_ptr->length)) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+ jpegr_uncompressed_struct yuv420_image{};
+ yuv420_image.data = jpeg_dec_obj_yuv420.getDecompressedImagePtr();
+ yuv420_image.width = jpeg_dec_obj_yuv420.getDecompressedImageWidth();
+ yuv420_image.height = jpeg_dec_obj_yuv420.getDecompressedImageHeight();
+ yuv420_image.colorGamut = yuv420jpg_image_ptr->colorGamut;
+ if (yuv420_image.luma_stride == 0) yuv420_image.luma_stride = yuv420_image.width;
+ if (!yuv420_image.chroma_data) {
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ yuv420_image.chroma_data = data + yuv420_image.luma_stride * p010_image.height;
+ yuv420_image.chroma_stride = yuv420_image.luma_stride >> 1;
+ }
+
+ if (p010_image_ptr->width != yuv420_image.width ||
+ p010_image_ptr->height != yuv420_image.height) {
+ return ERROR_JPEGR_RESOLUTION_MISMATCH;
+ }
+
+ // gain map
+ ultrahdr_metadata_struct metadata;
+ metadata.version = kJpegrVersion;
+ jpegr_uncompressed_struct gainmap_image;
+ JPEGR_CHECK(generateGainMap(&yuv420_image, &p010_image, hdr_tf, &metadata, &gainmap_image,
+ true /* sdr_is_601 */));
+ std::unique_ptr<uint8_t[]> map_data;
+ map_data.reset(reinterpret_cast<uint8_t*>(gainmap_image.data));
+
+ // compress gain map
+ JpegEncoderHelper jpeg_enc_obj_gm;
+ JPEGR_CHECK(compressGainMap(&gainmap_image, &jpeg_enc_obj_gm));
+ jpegr_compressed_struct gainmapjpg_image;
+ gainmapjpg_image.data = jpeg_enc_obj_gm.getCompressedImagePtr();
+ gainmapjpg_image.length = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ gainmapjpg_image.maxLength = static_cast<int>(jpeg_enc_obj_gm.getCompressedImageSize());
+ gainmapjpg_image.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+
+ return encodeJPEGR(yuv420jpg_image_ptr, &gainmapjpg_image, &metadata, dest);
+}
+
+/* Encode API-4 */
+status_t JpegR::encodeJPEGR(jr_compressed_ptr yuv420jpg_image_ptr,
+ jr_compressed_ptr gainmapjpg_image_ptr, ultrahdr_metadata_ptr metadata,
+ jr_compressed_ptr dest) {
+ if (yuv420jpg_image_ptr == nullptr || yuv420jpg_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed jpeg image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (gainmapjpg_image_ptr == nullptr || gainmapjpg_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed gain map");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (dest == nullptr || dest->data == nullptr) {
+ ALOGE("received nullptr for destination");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+
+ // We just want to check if ICC is present, so don't do a full decode. Note,
+ // this doesn't verify that the ICC is valid.
+ JpegDecoderHelper decoder;
+ std::vector<uint8_t> icc;
+ decoder.getCompressedImageParameters(yuv420jpg_image_ptr->data, yuv420jpg_image_ptr->length,
+ /* pWidth */ nullptr, /* pHeight */ nullptr, &icc,
+ /* exifData */ nullptr);
+
+ // Add ICC if not already present.
+ if (icc.size() > 0) {
+ JPEGR_CHECK(appendGainMap(yuv420jpg_image_ptr, gainmapjpg_image_ptr, /* exif */ nullptr,
+ /* icc */ nullptr, /* icc size */ 0, metadata, dest));
+ } else {
+ std::shared_ptr<DataStruct> newIcc =
+ IccHelper::writeIccProfile(ULTRAHDR_TF_SRGB, yuv420jpg_image_ptr->colorGamut);
+ JPEGR_CHECK(appendGainMap(yuv420jpg_image_ptr, gainmapjpg_image_ptr, /* exif */ nullptr,
+ newIcc->getData(), newIcc->getLength(), metadata, dest));
+ }
+
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::getJPEGRInfo(jr_compressed_ptr jpegr_image_ptr, jr_info_ptr jpeg_image_info_ptr) {
+ if (jpegr_image_ptr == nullptr || jpegr_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed jpegr image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (jpeg_image_info_ptr == nullptr) {
+ ALOGE("received nullptr for compressed jpegr info struct");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+
+ jpegr_compressed_struct primary_image, gainmap_image;
+ status_t status = extractPrimaryImageAndGainMap(jpegr_image_ptr, &primary_image, &gainmap_image);
+ if (status != JPEGR_NO_ERROR && status != ERROR_JPEGR_GAIN_MAP_IMAGE_NOT_FOUND) {
+ return status;
+ }
+
+ JpegDecoderHelper jpeg_dec_obj_hdr;
+ if (!jpeg_dec_obj_hdr.getCompressedImageParameters(
+ primary_image.data, primary_image.length, &jpeg_image_info_ptr->width,
+ &jpeg_image_info_ptr->height, jpeg_image_info_ptr->iccData,
+ jpeg_image_info_ptr->exifData)) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+
+ return status;
+}
+
+/* Decode API */
+status_t JpegR::decodeJPEGR(jr_compressed_ptr jpegr_image_ptr, jr_uncompressed_ptr dest,
+ float max_display_boost, jr_exif_ptr exif,
+ ultrahdr_output_format output_format,
+ jr_uncompressed_ptr gainmap_image_ptr, ultrahdr_metadata_ptr metadata) {
+ if (jpegr_image_ptr == nullptr || jpegr_image_ptr->data == nullptr) {
+ ALOGE("received nullptr for compressed jpegr image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (dest == nullptr || dest->data == nullptr) {
+ ALOGE("received nullptr for dest image");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (max_display_boost < 1.0f) {
+ ALOGE("received bad value for max_display_boost %f", max_display_boost);
+ return ERROR_JPEGR_INVALID_DISPLAY_BOOST;
+ }
+ if (exif != nullptr && exif->data == nullptr) {
+ ALOGE("received nullptr address for exif data");
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (output_format <= ULTRAHDR_OUTPUT_UNSPECIFIED || output_format > ULTRAHDR_OUTPUT_MAX) {
+ ALOGE("received bad value for output format %d", output_format);
+ return ERROR_JPEGR_INVALID_OUTPUT_FORMAT;
+ }
+
+ jpegr_compressed_struct primary_jpeg_image, gainmap_jpeg_image;
+ status_t status =
+ extractPrimaryImageAndGainMap(jpegr_image_ptr, &primary_jpeg_image, &gainmap_jpeg_image);
+ if (status != JPEGR_NO_ERROR) {
+ if (output_format != ULTRAHDR_OUTPUT_SDR || status != ERROR_JPEGR_GAIN_MAP_IMAGE_NOT_FOUND) {
+ ALOGE("received invalid compressed jpegr image");
+ return status;
+ }
+ }
+
+ JpegDecoderHelper jpeg_dec_obj_yuv420;
+ if (!jpeg_dec_obj_yuv420.decompressImage(primary_jpeg_image.data, primary_jpeg_image.length,
+ (output_format == ULTRAHDR_OUTPUT_SDR))) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+
+ if (output_format == ULTRAHDR_OUTPUT_SDR) {
+ if ((jpeg_dec_obj_yuv420.getDecompressedImageWidth() *
+ jpeg_dec_obj_yuv420.getDecompressedImageHeight() * 4) >
+ jpeg_dec_obj_yuv420.getDecompressedImageSize()) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+ } else {
+ if ((jpeg_dec_obj_yuv420.getDecompressedImageWidth() *
+ jpeg_dec_obj_yuv420.getDecompressedImageHeight() * 3 / 2) >
+ jpeg_dec_obj_yuv420.getDecompressedImageSize()) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+ }
+
+ if (exif != nullptr) {
+ if (exif->data == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (exif->length < jpeg_dec_obj_yuv420.getEXIFSize()) {
+ return ERROR_JPEGR_BUFFER_TOO_SMALL;
+ }
+ memcpy(exif->data, jpeg_dec_obj_yuv420.getEXIFPtr(), jpeg_dec_obj_yuv420.getEXIFSize());
+ exif->length = jpeg_dec_obj_yuv420.getEXIFSize();
+ }
+
+ if (output_format == ULTRAHDR_OUTPUT_SDR) {
+ dest->width = jpeg_dec_obj_yuv420.getDecompressedImageWidth();
+ dest->height = jpeg_dec_obj_yuv420.getDecompressedImageHeight();
+ memcpy(dest->data, jpeg_dec_obj_yuv420.getDecompressedImagePtr(),
+ dest->width * dest->height * 4);
+ return JPEGR_NO_ERROR;
+ }
+
+ JpegDecoderHelper jpeg_dec_obj_gm;
+ if (!jpeg_dec_obj_gm.decompressImage(gainmap_jpeg_image.data, gainmap_jpeg_image.length)) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+ if ((jpeg_dec_obj_gm.getDecompressedImageWidth() * jpeg_dec_obj_gm.getDecompressedImageHeight()) >
+ jpeg_dec_obj_gm.getDecompressedImageSize()) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+
+ jpegr_uncompressed_struct gainmap_image;
+ gainmap_image.data = jpeg_dec_obj_gm.getDecompressedImagePtr();
+ gainmap_image.width = jpeg_dec_obj_gm.getDecompressedImageWidth();
+ gainmap_image.height = jpeg_dec_obj_gm.getDecompressedImageHeight();
+
+ if (gainmap_image_ptr != nullptr) {
+ gainmap_image_ptr->width = gainmap_image.width;
+ gainmap_image_ptr->height = gainmap_image.height;
+ int size = gainmap_image_ptr->width * gainmap_image_ptr->height;
+ gainmap_image_ptr->data = malloc(size);
+ memcpy(gainmap_image_ptr->data, gainmap_image.data, size);
+ }
+
+ ultrahdr_metadata_struct uhdr_metadata;
+ if (!getMetadataFromXMP(static_cast<uint8_t*>(jpeg_dec_obj_gm.getXMPPtr()),
+ jpeg_dec_obj_gm.getXMPSize(), &uhdr_metadata)) {
+ return ERROR_JPEGR_METADATA_ERROR;
+ }
+
+ if (metadata != nullptr) {
+ metadata->version = uhdr_metadata.version;
+ metadata->minContentBoost = uhdr_metadata.minContentBoost;
+ metadata->maxContentBoost = uhdr_metadata.maxContentBoost;
+ metadata->gamma = uhdr_metadata.gamma;
+ metadata->offsetSdr = uhdr_metadata.offsetSdr;
+ metadata->offsetHdr = uhdr_metadata.offsetHdr;
+ metadata->hdrCapacityMin = uhdr_metadata.hdrCapacityMin;
+ metadata->hdrCapacityMax = uhdr_metadata.hdrCapacityMax;
+ }
+
+ jpegr_uncompressed_struct yuv420_image;
+ yuv420_image.data = jpeg_dec_obj_yuv420.getDecompressedImagePtr();
+ yuv420_image.width = jpeg_dec_obj_yuv420.getDecompressedImageWidth();
+ yuv420_image.height = jpeg_dec_obj_yuv420.getDecompressedImageHeight();
+ yuv420_image.colorGamut = IccHelper::readIccColorGamut(jpeg_dec_obj_yuv420.getICCPtr(),
+ jpeg_dec_obj_yuv420.getICCSize());
+ yuv420_image.luma_stride = yuv420_image.width;
+ uint8_t* data = reinterpret_cast<uint8_t*>(yuv420_image.data);
+ yuv420_image.chroma_data = data + yuv420_image.luma_stride * yuv420_image.height;
+ yuv420_image.chroma_stride = yuv420_image.width >> 1;
+
+ JPEGR_CHECK(applyGainMap(&yuv420_image, &gainmap_image, &uhdr_metadata, output_format,
+ max_display_boost, dest));
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::compressGainMap(jr_uncompressed_ptr gainmap_image_ptr,
+ JpegEncoderHelper* jpeg_enc_obj_ptr) {
+ if (gainmap_image_ptr == nullptr || jpeg_enc_obj_ptr == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+
+ // Don't need to convert YUV to Bt601 since single channel
+ if (!jpeg_enc_obj_ptr->compressImage(reinterpret_cast<uint8_t*>(gainmap_image_ptr->data), nullptr,
+ gainmap_image_ptr->width, gainmap_image_ptr->height,
+ gainmap_image_ptr->luma_stride, 0, kMapCompressQuality,
+ nullptr, 0)) {
+ return ERROR_JPEGR_ENCODE_ERROR;
+ }
+
+ return JPEGR_NO_ERROR;
+}
+
+const int kJobSzInRows = 16;
+static_assert(kJobSzInRows > 0 && kJobSzInRows % kMapDimensionScaleFactor == 0,
+ "align job size to kMapDimensionScaleFactor");
+
+class JobQueue {
+ public:
+ bool dequeueJob(size_t& rowStart, size_t& rowEnd);
+ void enqueueJob(size_t rowStart, size_t rowEnd);
+ void markQueueForEnd();
+ void reset();
+
+ private:
+ bool mQueuedAllJobs = false;
+ std::deque<std::tuple<size_t, size_t>> mJobs;
+ std::mutex mMutex;
+ std::condition_variable mCv;
+};
+
+bool JobQueue::dequeueJob(size_t& rowStart, size_t& rowEnd) {
+ std::unique_lock<std::mutex> lock{mMutex};
+ while (true) {
+ if (mJobs.empty()) {
+ if (mQueuedAllJobs) {
+ return false;
+ } else {
+ mCv.wait_for(lock, std::chrono::milliseconds(100));
+ }
+ } else {
+ auto it = mJobs.begin();
+ rowStart = std::get<0>(*it);
+ rowEnd = std::get<1>(*it);
+ mJobs.erase(it);
+ return true;
+ }
+ }
+ return false;
+}
+
+void JobQueue::enqueueJob(size_t rowStart, size_t rowEnd) {
+ std::unique_lock<std::mutex> lock{mMutex};
+ mJobs.push_back(std::make_tuple(rowStart, rowEnd));
+ lock.unlock();
+ mCv.notify_one();
+}
+
+void JobQueue::markQueueForEnd() {
+ std::unique_lock<std::mutex> lock{mMutex};
+ mQueuedAllJobs = true;
+ lock.unlock();
+ mCv.notify_all();
+}
+
+void JobQueue::reset() {
+ std::unique_lock<std::mutex> lock{mMutex};
+ mJobs.clear();
+ mQueuedAllJobs = false;
+}
+
+status_t JpegR::generateGainMap(jr_uncompressed_ptr yuv420_image_ptr,
+ jr_uncompressed_ptr p010_image_ptr,
+ ultrahdr_transfer_function hdr_tf, ultrahdr_metadata_ptr metadata,
+ jr_uncompressed_ptr dest, bool sdr_is_601) {
+ if (yuv420_image_ptr == nullptr || p010_image_ptr == nullptr || metadata == nullptr ||
+ dest == nullptr || yuv420_image_ptr->data == nullptr ||
+ yuv420_image_ptr->chroma_data == nullptr || p010_image_ptr->data == nullptr ||
+ p010_image_ptr->chroma_data == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (yuv420_image_ptr->width != p010_image_ptr->width ||
+ yuv420_image_ptr->height != p010_image_ptr->height) {
+ return ERROR_JPEGR_RESOLUTION_MISMATCH;
+ }
+ if (yuv420_image_ptr->colorGamut == ULTRAHDR_COLORGAMUT_UNSPECIFIED ||
+ p010_image_ptr->colorGamut == ULTRAHDR_COLORGAMUT_UNSPECIFIED) {
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+
+ size_t image_width = yuv420_image_ptr->width;
+ size_t image_height = yuv420_image_ptr->height;
+ size_t map_width = image_width / kMapDimensionScaleFactor;
+ size_t map_height = image_height / kMapDimensionScaleFactor;
+
+ dest->data = new uint8_t[map_width * map_height];
+ dest->width = map_width;
+ dest->height = map_height;
+ dest->colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+ dest->luma_stride = map_width;
+ dest->chroma_data = nullptr;
+ dest->chroma_stride = 0;
+ std::unique_ptr<uint8_t[]> map_data;
+ map_data.reset(reinterpret_cast<uint8_t*>(dest->data));
+
+ ColorTransformFn hdrInvOetf = nullptr;
+ float hdr_white_nits;
+ switch (hdr_tf) {
+ case ULTRAHDR_TF_LINEAR:
+ hdrInvOetf = identityConversion;
+ // Note: this will produce clipping if the input exceeds kHlgMaxNits.
+ // TODO: TF LINEAR will be deprecated.
+ hdr_white_nits = kHlgMaxNits;
+ break;
+ case ULTRAHDR_TF_HLG:
+#if USE_HLG_INVOETF_LUT
+ hdrInvOetf = hlgInvOetfLUT;
+#else
+ hdrInvOetf = hlgInvOetf;
+#endif
+ hdr_white_nits = kHlgMaxNits;
+ break;
+ case ULTRAHDR_TF_PQ:
+#if USE_PQ_INVOETF_LUT
+ hdrInvOetf = pqInvOetfLUT;
+#else
+ hdrInvOetf = pqInvOetf;
+#endif
+ hdr_white_nits = kPqMaxNits;
+ break;
+ default:
+ // Should be impossible to hit after input validation.
+ return ERROR_JPEGR_INVALID_TRANS_FUNC;
+ }
+
+ metadata->maxContentBoost = hdr_white_nits / kSdrWhiteNits;
+ metadata->minContentBoost = 1.0f;
+ metadata->gamma = 1.0f;
+ metadata->offsetSdr = 0.0f;
+ metadata->offsetHdr = 0.0f;
+ metadata->hdrCapacityMin = 1.0f;
+ metadata->hdrCapacityMax = metadata->maxContentBoost;
+
+ float log2MinBoost = log2(metadata->minContentBoost);
+ float log2MaxBoost = log2(metadata->maxContentBoost);
+
+ ColorTransformFn hdrGamutConversionFn =
+ getHdrConversionFn(yuv420_image_ptr->colorGamut, p010_image_ptr->colorGamut);
+
+ ColorCalculationFn luminanceFn = nullptr;
+ ColorTransformFn sdrYuvToRgbFn = nullptr;
+ switch (yuv420_image_ptr->colorGamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ luminanceFn = srgbLuminance;
+ sdrYuvToRgbFn = srgbYuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ luminanceFn = p3Luminance;
+ sdrYuvToRgbFn = p3YuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ luminanceFn = bt2100Luminance;
+ sdrYuvToRgbFn = bt2100YuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ // Should be impossible to hit after input validation.
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ if (sdr_is_601) {
+ sdrYuvToRgbFn = p3YuvToRgb;
+ }
+
+ ColorTransformFn hdrYuvToRgbFn = nullptr;
+ switch (p010_image_ptr->colorGamut) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ hdrYuvToRgbFn = srgbYuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ hdrYuvToRgbFn = p3YuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ hdrYuvToRgbFn = bt2100YuvToRgb;
+ break;
+ case ULTRAHDR_COLORGAMUT_UNSPECIFIED:
+ // Should be impossible to hit after input validation.
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+
+ const int threads = (std::min)(GetCPUCoreCount(), 4);
+ size_t rowStep = threads == 1 ? image_height : kJobSzInRows;
+ JobQueue jobQueue;
+
+ std::function<void()> generateMap = [yuv420_image_ptr, p010_image_ptr, metadata, dest, hdrInvOetf,
+ hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn,
+ hdrYuvToRgbFn, hdr_white_nits, log2MinBoost, log2MaxBoost,
+ &jobQueue]() -> void {
+ size_t rowStart, rowEnd;
+ while (jobQueue.dequeueJob(rowStart, rowEnd)) {
+ for (size_t y = rowStart; y < rowEnd; ++y) {
+ for (size_t x = 0; x < dest->width; ++x) {
+ Color sdr_yuv_gamma = sampleYuv420(yuv420_image_ptr, kMapDimensionScaleFactor, x, y);
+ Color sdr_rgb_gamma = sdrYuvToRgbFn(sdr_yuv_gamma);
+ // We are assuming the SDR input is always sRGB transfer.
+#if USE_SRGB_INVOETF_LUT
+ Color sdr_rgb = srgbInvOetfLUT(sdr_rgb_gamma);
+#else
+ Color sdr_rgb = srgbInvOetf(sdr_rgb_gamma);
+#endif
+ float sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits;
+
+ Color hdr_yuv_gamma = sampleP010(p010_image_ptr, kMapDimensionScaleFactor, x, y);
+ Color hdr_rgb_gamma = hdrYuvToRgbFn(hdr_yuv_gamma);
+ Color hdr_rgb = hdrInvOetf(hdr_rgb_gamma);
+ hdr_rgb = hdrGamutConversionFn(hdr_rgb);
+ float hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
+
+ size_t pixel_idx = x + y * dest->width;
+ reinterpret_cast<uint8_t*>(dest->data)[pixel_idx] =
+ encodeGain(sdr_y_nits, hdr_y_nits, metadata, log2MinBoost, log2MaxBoost);
+ }
+ }
+ }
+ };
+
+ // generate map
+ std::vector<std::thread> workers;
+ for (int th = 0; th < threads - 1; th++) {
+ workers.push_back(std::thread(generateMap));
+ }
+
+ rowStep = (threads == 1 ? image_height : kJobSzInRows) / kMapDimensionScaleFactor;
+ for (size_t rowStart = 0; rowStart < map_height;) {
+ size_t rowEnd = (std::min)(rowStart + rowStep, map_height);
+ jobQueue.enqueueJob(rowStart, rowEnd);
+ rowStart = rowEnd;
+ }
+ jobQueue.markQueueForEnd();
+ generateMap();
+ std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });
+
+ map_data.release();
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::applyGainMap(jr_uncompressed_ptr yuv420_image_ptr,
+ jr_uncompressed_ptr gainmap_image_ptr, ultrahdr_metadata_ptr metadata,
+ ultrahdr_output_format output_format, float max_display_boost,
+ jr_uncompressed_ptr dest) {
+ if (yuv420_image_ptr == nullptr || gainmap_image_ptr == nullptr || metadata == nullptr ||
+ dest == nullptr || yuv420_image_ptr->data == nullptr ||
+ yuv420_image_ptr->chroma_data == nullptr || gainmap_image_ptr->data == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (metadata->version.compare(kJpegrVersion)) {
+ ALOGE("Unsupported metadata version: %s", metadata->version.c_str());
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->gamma != 1.0f) {
+ ALOGE("Unsupported metadata gamma: %f", metadata->gamma);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->offsetSdr != 0.0f || metadata->offsetHdr != 0.0f) {
+ ALOGE("Unsupported metadata offset sdr, hdr: %f, %f", metadata->offsetSdr, metadata->offsetHdr);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->hdrCapacityMin != metadata->minContentBoost ||
+ metadata->hdrCapacityMax != metadata->maxContentBoost) {
+ ALOGE("Unsupported metadata hdr capacity min, max: %f, %f", metadata->hdrCapacityMin,
+ metadata->hdrCapacityMax);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+
+ // TODO: remove once map scaling factor is computed based on actual map dims
+ size_t image_width = yuv420_image_ptr->width;
+ size_t image_height = yuv420_image_ptr->height;
+ size_t map_width = image_width / kMapDimensionScaleFactor;
+ size_t map_height = image_height / kMapDimensionScaleFactor;
+ if (map_width != gainmap_image_ptr->width || map_height != gainmap_image_ptr->height) {
+ ALOGE(
+ "gain map dimensions and primary image dimensions are not to scale, computed gain map "
+ "resolution is %zux%zu, received gain map resolution is %zux%zu",
+ map_width, map_height, gainmap_image_ptr->width, gainmap_image_ptr->height);
+ return ERROR_JPEGR_RESOLUTION_MISMATCH;
+ }
+
+ dest->width = yuv420_image_ptr->width;
+ dest->height = yuv420_image_ptr->height;
+ ShepardsIDW idwTable(kMapDimensionScaleFactor);
+ float display_boost = (std::min)(max_display_boost, metadata->maxContentBoost);
+ GainLUT gainLUT(metadata, display_boost);
+
+ JobQueue jobQueue;
+ std::function<void()> applyRecMap = [yuv420_image_ptr, gainmap_image_ptr, dest, &jobQueue,
+ &idwTable, output_format, &gainLUT,
+ display_boost]() -> void {
+ size_t width = yuv420_image_ptr->width;
+
+ size_t rowStart, rowEnd;
+ while (jobQueue.dequeueJob(rowStart, rowEnd)) {
+ for (size_t y = rowStart; y < rowEnd; ++y) {
+ for (size_t x = 0; x < width; ++x) {
+ Color yuv_gamma_sdr = getYuv420Pixel(yuv420_image_ptr, x, y);
+ // Assuming the sdr image is a decoded JPEG, we should always use Rec.601 YUV coefficients
+ Color rgb_gamma_sdr = p3YuvToRgb(yuv_gamma_sdr);
+ // We are assuming the SDR base image is always sRGB transfer.
+#if USE_SRGB_INVOETF_LUT
+ Color rgb_sdr = srgbInvOetfLUT(rgb_gamma_sdr);
+#else
+ Color rgb_sdr = srgbInvOetf(rgb_gamma_sdr);
+#endif
+ float gain;
+ // TODO: determine map scaling factor based on actual map dims
+ size_t map_scale_factor = kMapDimensionScaleFactor;
+ // TODO: If map_scale_factor is guaranteed to be an integer, then remove the following.
+ // Currently map_scale_factor is of type size_t, but it could be changed to a float
+ // later.
+ if (map_scale_factor != floorf(map_scale_factor)) {
+ gain = sampleMap(gainmap_image_ptr, map_scale_factor, x, y);
+ } else {
+ gain = sampleMap(gainmap_image_ptr, map_scale_factor, x, y, idwTable);
+ }
+
+#if USE_APPLY_GAIN_LUT
+ Color rgb_hdr = applyGainLUT(rgb_sdr, gain, gainLUT);
+#else
+ Color rgb_hdr = applyGain(rgb_sdr, gain, metadata, display_boost);
+#endif
+ rgb_hdr = rgb_hdr / display_boost;
+ size_t pixel_idx = x + y * width;
+
+ switch (output_format) {
+ case ULTRAHDR_OUTPUT_HDR_LINEAR: {
+ uint64_t rgba_f16 = colorToRgbaF16(rgb_hdr);
+ reinterpret_cast<uint64_t*>(dest->data)[pixel_idx] = rgba_f16;
+ break;
+ }
+ case ULTRAHDR_OUTPUT_HDR_HLG: {
+#if USE_HLG_OETF_LUT
+ ColorTransformFn hdrOetf = hlgOetfLUT;
+#else
+ ColorTransformFn hdrOetf = hlgOetf;
+#endif
+ Color rgb_gamma_hdr = hdrOetf(rgb_hdr);
+ uint32_t rgba_1010102 = colorToRgba1010102(rgb_gamma_hdr);
+ reinterpret_cast<uint32_t*>(dest->data)[pixel_idx] = rgba_1010102;
+ break;
+ }
+ case ULTRAHDR_OUTPUT_HDR_PQ: {
+#if USE_PQ_OETF_LUT
+ ColorTransformFn hdrOetf = pqOetfLUT;
+#else
+ ColorTransformFn hdrOetf = pqOetf;
+#endif
+ Color rgb_gamma_hdr = hdrOetf(rgb_hdr);
+ uint32_t rgba_1010102 = colorToRgba1010102(rgb_gamma_hdr);
+ reinterpret_cast<uint32_t*>(dest->data)[pixel_idx] = rgba_1010102;
+ break;
+ }
+ default: {
+ }
+ // Should be impossible to hit after input validation.
+ }
+ }
+ }
+ }
+ };
+
+ const int threads = (std::min)(GetCPUCoreCount(), 4);
+ std::vector<std::thread> workers;
+ for (int th = 0; th < threads - 1; th++) {
+ workers.push_back(std::thread(applyRecMap));
+ }
+ const int rowStep = threads == 1 ? yuv420_image_ptr->height : kJobSzInRows;
+ for (size_t rowStart = 0; rowStart < yuv420_image_ptr->height;) {
+ int rowEnd = (std::min)(rowStart + rowStep, yuv420_image_ptr->height);
+ jobQueue.enqueueJob(rowStart, rowEnd);
+ rowStart = rowEnd;
+ }
+ jobQueue.markQueueForEnd();
+ applyRecMap();
+ std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::extractPrimaryImageAndGainMap(jr_compressed_ptr jpegr_image_ptr,
+ jr_compressed_ptr primary_jpg_image_ptr,
+ jr_compressed_ptr gainmap_jpg_image_ptr) {
+ if (jpegr_image_ptr == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+
+ MessageHandler msg_handler;
+ msg_handler.SetMessageWriter(make_unique<AlogMessageWriter>(AlogMessageWriter()));
+ std::shared_ptr<DataSegment> seg = DataSegment::Create(
+ DataRange(0, jpegr_image_ptr->length), static_cast<const uint8_t*>(jpegr_image_ptr->data),
+ DataSegment::BufferDispositionPolicy::kDontDelete);
+ DataSegmentDataSource data_source(seg);
+ JpegInfoBuilder jpeg_info_builder;
+ jpeg_info_builder.SetImageLimit(2);
+ JpegScanner jpeg_scanner(&msg_handler);
+ jpeg_scanner.Run(&data_source, &jpeg_info_builder);
+ data_source.Reset();
+
+ if (jpeg_scanner.HasError()) {
+ return JPEGR_UNKNOWN_ERROR;
+ }
+
+ const auto& jpeg_info = jpeg_info_builder.GetInfo();
+ const auto& image_ranges = jpeg_info.GetImageRanges();
+
+ if (image_ranges.empty()) {
+ return ERROR_JPEGR_NO_IMAGES_FOUND;
+ }
+
+ if (primary_jpg_image_ptr != nullptr) {
+ primary_jpg_image_ptr->data =
+ static_cast<uint8_t*>(jpegr_image_ptr->data) + image_ranges[0].GetBegin();
+ primary_jpg_image_ptr->length = image_ranges[0].GetLength();
+ }
+
+ if (image_ranges.size() == 1) {
+ return ERROR_JPEGR_GAIN_MAP_IMAGE_NOT_FOUND;
+ }
+
+ if (gainmap_jpg_image_ptr != nullptr) {
+ gainmap_jpg_image_ptr->data =
+ static_cast<uint8_t*>(jpegr_image_ptr->data) + image_ranges[1].GetBegin();
+ gainmap_jpg_image_ptr->length = image_ranges[1].GetLength();
+ }
+
+ // TODO: choose primary image and gain map image carefully
+ if (image_ranges.size() > 2) {
+ ALOGW("Number of jpeg images present %d, primary, gain map images may not be correctly chosen",
+ (int)image_ranges.size());
+ }
+
+ return JPEGR_NO_ERROR;
+}
+
+// JPEG/R structure:
+// SOI (ff d8)
+//
+// (Optional, if EXIF package is from outside (Encode API-0 API-1), or if EXIF package presents
+// in the JPEG input (Encode API-2, API-3, API-4))
+// APP1 (ff e1)
+// 2 bytes of length (2 + length of exif package)
+// EXIF package (this includes the first two bytes representing the package length)
+//
+// (Required, XMP package) APP1 (ff e1)
+// 2 bytes of length (2 + 29 + length of xmp package)
+// name space ("http://ns.adobe.com/xap/1.0/\0")
+// XMP
+//
+// (Required, MPF package) APP2 (ff e2)
+// 2 bytes of length
+// MPF
+//
+// (Required) primary image (without the first two bytes (SOI) and EXIF, may have other packages)
+//
+// SOI (ff d8)
+//
+// (Required, XMP package) APP1 (ff e1)
+// 2 bytes of length (2 + 29 + length of xmp package)
+// name space ("http://ns.adobe.com/xap/1.0/\0")
+// XMP
+//
+// (Required) secondary image (the gain map, without the first two bytes (SOI))
+//
+// 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 JpegR::appendGainMap(jr_compressed_ptr primary_jpg_image_ptr,
+ jr_compressed_ptr gainmap_jpg_image_ptr, jr_exif_ptr pExif,
+ void* pIcc, size_t icc_size, ultrahdr_metadata_ptr metadata,
+ jr_compressed_ptr dest) {
+ if (primary_jpg_image_ptr == nullptr || gainmap_jpg_image_ptr == nullptr || metadata == nullptr ||
+ dest == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (metadata->version.compare("1.0")) {
+ ALOGE("received bad value for version: %s", metadata->version.c_str());
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->maxContentBoost < metadata->minContentBoost) {
+ ALOGE("received bad value for content boost min %f, max %f", metadata->minContentBoost,
+ metadata->maxContentBoost);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->hdrCapacityMax < metadata->hdrCapacityMin || metadata->hdrCapacityMin < 1.0f) {
+ ALOGE("received bad value for hdr capacity min %f, max %f", metadata->hdrCapacityMin,
+ metadata->hdrCapacityMax);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->offsetSdr < 0.0f || metadata->offsetHdr < 0.0f) {
+ ALOGE("received bad value for offset sdr %f, hdr %f", metadata->offsetSdr, metadata->offsetHdr);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+ if (metadata->gamma <= 0.0f) {
+ ALOGE("received bad value for gamma %f", metadata->gamma);
+ return ERROR_JPEGR_BAD_METADATA;
+ }
+
+ const string nameSpace = "http://ns.adobe.com/xap/1.0/";
+ const int nameSpaceLength = nameSpace.size() + 1; // need to count the null terminator
+
+ // calculate secondary image length first, because the length will be written into the primary
+ // image xmp
+ const string xmp_secondary = generateXmpForSecondaryImage(*metadata);
+ // xmp_secondary_length = 2 bytes representing the length of the package +
+ // + nameSpaceLength = 29 bytes length
+ // + length of xmp packet = xmp_secondary.size()
+ const int xmp_secondary_length = 2 + nameSpaceLength + xmp_secondary.size();
+ const int secondary_image_size = 2 /* 2 bytes length of APP1 sign */
+ + xmp_secondary_length + gainmap_jpg_image_ptr->length;
+ // primary image
+ const string xmp_primary = generateXmpForPrimaryImage(secondary_image_size, *metadata);
+ // same as primary
+ const int xmp_primary_length = 2 + nameSpaceLength + xmp_primary.size();
+
+ // Check if EXIF package presents in the JPEG input.
+ // If so, extract and remove the EXIF package.
+ JpegDecoderHelper decoder;
+ if (!decoder.extractEXIF(primary_jpg_image_ptr->data, primary_jpg_image_ptr->length)) {
+ return ERROR_JPEGR_DECODE_ERROR;
+ }
+ jpegr_exif_struct exif_from_jpg;
+ exif_from_jpg.data = nullptr;
+ exif_from_jpg.length = 0;
+ jpegr_compressed_struct new_jpg_image;
+ new_jpg_image.data = nullptr;
+ new_jpg_image.length = 0;
+ new_jpg_image.maxLength = 0;
+ new_jpg_image.colorGamut = ULTRAHDR_COLORGAMUT_UNSPECIFIED;
+ std::unique_ptr<uint8_t[]> dest_data;
+ if (decoder.getEXIFPos() >= 0) {
+ if (pExif != nullptr) {
+ ALOGE("received EXIF from outside while the primary image already contains EXIF");
+ return ERROR_JPEGR_MULTIPLE_EXIFS_RECEIVED;
+ }
+ copyJpegWithoutExif(&new_jpg_image, primary_jpg_image_ptr, decoder.getEXIFPos(),
+ decoder.getEXIFSize());
+ dest_data.reset(reinterpret_cast<uint8_t*>(new_jpg_image.data));
+ exif_from_jpg.data = decoder.getEXIFPtr();
+ exif_from_jpg.length = decoder.getEXIFSize();
+ pExif = &exif_from_jpg;
+ }
+
+ jr_compressed_ptr final_primary_jpg_image_ptr =
+ new_jpg_image.length == 0 ? primary_jpg_image_ptr : &new_jpg_image;
+
+ int pos = 0;
+ // Begin primary image
+ // 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
+ if (pExif != nullptr) {
+ const int length = 2 + pExif->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, pExif->data, pExif->length, pos));
+ }
+
+ // Prepare and write XMP
+ {
+ const int length = xmp_primary_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, (void*)nameSpace.c_str(), nameSpaceLength, pos));
+ JPEGR_CHECK(Write(dest, (void*)xmp_primary.c_str(), xmp_primary.size(), pos));
+ }
+
+ // Write ICC
+ if (pIcc != nullptr && icc_size > 0) {
+ const int length = icc_size + 2;
+ 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::kAPP2, 1, pos));
+ JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
+ JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
+ JPEGR_CHECK(Write(dest, pIcc, icc_size, pos));
+ }
+
+ // Prepare and write MPF
+ {
+ const int length = 2 + calculateMpfSize();
+ const uint8_t lengthH = ((length >> 8) & 0xff);
+ const uint8_t lengthL = (length & 0xff);
+ int primary_image_size = pos + length + final_primary_jpg_image_ptr->length;
+ // between APP2 + package size + signature
+ // ff e2 00 58 4d 50 46 00
+ // 2 + 2 + 4 = 8 (bytes)
+ // and ff d8 sign of the secondary image
+ int secondary_image_offset = primary_image_size - pos - 8;
+ std::shared_ptr<DataStruct> mpf = generateMpf(primary_image_size, 0, /* primary_image_offset */
+ secondary_image_size, secondary_image_offset);
+ JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
+ JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos));
+ JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
+ JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
+ JPEGR_CHECK(Write(dest, (void*)mpf->getData(), mpf->getLength(), pos));
+ }
+
+ // Write primary image
+ JPEGR_CHECK(Write(dest, (uint8_t*)final_primary_jpg_image_ptr->data + 2,
+ final_primary_jpg_image_ptr->length - 2, pos));
+ // Finish primary image
+
+ // Begin secondary image (gain map)
+ // 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));
+
+ // Prepare and write XMP
+ {
+ const int length = xmp_secondary_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, (void*)nameSpace.c_str(), nameSpaceLength, pos));
+ JPEGR_CHECK(Write(dest, (void*)xmp_secondary.c_str(), xmp_secondary.size(), pos));
+ }
+
+ // Write secondary image
+ JPEGR_CHECK(Write(dest, (uint8_t*)gainmap_jpg_image_ptr->data + 2,
+ gainmap_jpg_image_ptr->length - 2, pos));
+
+ // Set back length
+ dest->length = pos;
+
+ // Done!
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::toneMap(jr_uncompressed_ptr src, jr_uncompressed_ptr dest) {
+ if (src == nullptr || dest == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (src->width != dest->width || src->height != dest->height) {
+ return ERROR_JPEGR_RESOLUTION_MISMATCH;
+ }
+ uint16_t* src_y_data = reinterpret_cast<uint16_t*>(src->data);
+ uint8_t* dst_y_data = reinterpret_cast<uint8_t*>(dest->data);
+ for (size_t y = 0; y < src->height; ++y) {
+ uint16_t* src_y_row = src_y_data + y * src->luma_stride;
+ uint8_t* dst_y_row = dst_y_data + y * dest->luma_stride;
+ for (size_t x = 0; x < src->width; ++x) {
+ uint16_t y_uint = src_y_row[x] >> 6;
+ dst_y_row[x] = static_cast<uint8_t>((y_uint >> 2) & 0xff);
+ }
+ if (dest->width != dest->luma_stride) {
+ memset(dst_y_row + dest->width, 0, dest->luma_stride - dest->width);
+ }
+ }
+ uint16_t* src_uv_data = reinterpret_cast<uint16_t*>(src->chroma_data);
+ uint8_t* dst_u_data = reinterpret_cast<uint8_t*>(dest->chroma_data);
+ size_t dst_v_offset = (dest->chroma_stride * dest->height / 2);
+ uint8_t* dst_v_data = dst_u_data + dst_v_offset;
+ for (size_t y = 0; y < src->height / 2; ++y) {
+ uint16_t* src_uv_row = src_uv_data + y * src->chroma_stride;
+ uint8_t* dst_u_row = dst_u_data + y * dest->chroma_stride;
+ uint8_t* dst_v_row = dst_v_data + y * dest->chroma_stride;
+ for (size_t x = 0; x < src->width / 2; ++x) {
+ uint16_t u_uint = src_uv_row[x << 1] >> 6;
+ uint16_t v_uint = src_uv_row[(x << 1) + 1] >> 6;
+ dst_u_row[x] = static_cast<uint8_t>((u_uint >> 2) & 0xff);
+ dst_v_row[x] = static_cast<uint8_t>((v_uint >> 2) & 0xff);
+ }
+ if (dest->width / 2 != dest->chroma_stride) {
+ memset(dst_u_row + dest->width / 2, 0, dest->chroma_stride - dest->width / 2);
+ memset(dst_v_row + dest->width / 2, 0, dest->chroma_stride - dest->width / 2);
+ }
+ }
+ dest->colorGamut = src->colorGamut;
+ return JPEGR_NO_ERROR;
+}
+
+status_t JpegR::convertYuv(jr_uncompressed_ptr image, ultrahdr_color_gamut src_encoding,
+ ultrahdr_color_gamut dest_encoding) {
+ if (image == nullptr) {
+ return ERROR_JPEGR_BAD_PTR;
+ }
+ if (src_encoding == ULTRAHDR_COLORGAMUT_UNSPECIFIED ||
+ dest_encoding == ULTRAHDR_COLORGAMUT_UNSPECIFIED) {
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+
+ ColorTransformFn conversionFn = nullptr;
+ switch (src_encoding) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ switch (dest_encoding) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ return JPEGR_NO_ERROR;
+ case ULTRAHDR_COLORGAMUT_P3:
+ conversionFn = yuv709To601;
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ conversionFn = yuv709To2100;
+ break;
+ default:
+ // Should be impossible to hit after input validation
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ switch (dest_encoding) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ conversionFn = yuv601To709;
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ return JPEGR_NO_ERROR;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ conversionFn = yuv601To2100;
+ break;
+ default:
+ // Should be impossible to hit after input validation
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ switch (dest_encoding) {
+ case ULTRAHDR_COLORGAMUT_BT709:
+ conversionFn = yuv2100To709;
+ break;
+ case ULTRAHDR_COLORGAMUT_P3:
+ conversionFn = yuv2100To601;
+ break;
+ case ULTRAHDR_COLORGAMUT_BT2100:
+ return JPEGR_NO_ERROR;
+ default:
+ // Should be impossible to hit after input validation
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+ break;
+ default:
+ // Should be impossible to hit after input validation
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+
+ if (conversionFn == nullptr) {
+ // Should be impossible to hit after input validation
+ return ERROR_JPEGR_INVALID_COLORGAMUT;
+ }
+
+ for (size_t y = 0; y < image->height / 2; ++y) {
+ for (size_t x = 0; x < image->width / 2; ++x) {
+ transformYuv420(image, x, y, conversionFn);
+ }
+ }
+
+ return JPEGR_NO_ERROR;
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/jpegrutils.cpp b/lib/src/jpegrutils.cpp
new file mode 100644
index 0000000..77cb26b
--- /dev/null
+++ b/lib/src/jpegrutils.cpp
@@ -0,0 +1,583 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+#include <cmath>
+
+#include "ultrahdr/ultrahdrcommon.h"
+#include "ultrahdr/jpegr.h"
+#include "ultrahdr/jpegrutils.h"
+
+#include "image_io/xml/xml_reader.h"
+#include "image_io/xml/xml_writer.h"
+#include "image_io/base/message_handler.h"
+#include "image_io/xml/xml_element_rules.h"
+#include "image_io/xml/xml_handler.h"
+#include "image_io/xml/xml_rule.h"
+
+using namespace photos_editing_formats::image_io;
+using namespace std;
+
+namespace ultrahdr {
+/*
+ * Helper function used for generating XMP metadata.
+ *
+ * @param prefix The prefix part of the name.
+ * @param suffix The suffix part of the name.
+ * @return A name of the form "prefix:suffix".
+ */
+static inline string Name(const string& prefix, const string& suffix) {
+ std::stringstream ss;
+ ss << prefix << ":" << suffix;
+ return ss.str();
+}
+
+DataStruct::DataStruct(int s) {
+ data = malloc(s);
+ length = s;
+ memset(data, 0, s);
+ writePos = 0;
+}
+
+DataStruct::~DataStruct() {
+ if (data != nullptr) {
+ free(data);
+ }
+}
+
+void* DataStruct::getData() { return data; }
+
+int DataStruct::getLength() { return length; }
+
+int DataStruct::getBytesWritten() { return writePos; }
+
+bool DataStruct::write8(uint8_t value) {
+ uint8_t v = value;
+ return write(&v, 1);
+}
+
+bool DataStruct::write16(uint16_t value) {
+ uint16_t v = value;
+ return write(&v, 2);
+}
+bool DataStruct::write32(uint32_t value) {
+ uint32_t v = value;
+ return write(&v, 4);
+}
+
+bool DataStruct::write(const void* src, int size) {
+ if (writePos + size > length) {
+ ALOGE("Writing out of boundary: write position: %d, size: %d, capacity: %d", writePos, size,
+ length);
+ return false;
+ }
+ memcpy((uint8_t*)data + writePos, src, size);
+ writePos += size;
+ return true;
+}
+
+/*
+ * Helper function used for writing data to destination.
+ */
+status_t Write(jr_compressed_ptr destination, const void* source, size_t length, int& position) {
+ if (position + length > destination->maxLength) {
+ return ERROR_JPEGR_BUFFER_TOO_SMALL;
+ }
+
+ memcpy((uint8_t*)destination->data + sizeof(uint8_t) * position, source, length);
+ position += length;
+ return JPEGR_NO_ERROR;
+}
+
+// Extremely simple XML Handler - just searches for interesting elements
+class XMPXmlHandler : public XmlHandler {
+ public:
+ XMPXmlHandler() : XmlHandler() {
+ state = NotStrarted;
+ versionFound = false;
+ minContentBoostFound = false;
+ maxContentBoostFound = false;
+ gammaFound = false;
+ offsetSdrFound = false;
+ offsetHdrFound = false;
+ hdrCapacityMinFound = false;
+ hdrCapacityMaxFound = false;
+ baseRenditionIsHdrFound = false;
+ }
+
+ enum ParseState { NotStrarted, Started, Done };
+
+ virtual DataMatchResult StartElement(const XmlTokenContext& context) {
+ string val;
+ if (context.BuildTokenValue(&val)) {
+ if (!val.compare(containerName)) {
+ state = Started;
+ } else {
+ if (state != Done) {
+ state = NotStrarted;
+ }
+ }
+ }
+ return context.GetResult();
+ }
+
+ virtual DataMatchResult FinishElement(const XmlTokenContext& context) {
+ if (state == Started) {
+ state = Done;
+ lastAttributeName = "";
+ }
+ return context.GetResult();
+ }
+
+ virtual DataMatchResult AttributeName(const XmlTokenContext& context) {
+ string val;
+ if (state == Started) {
+ if (context.BuildTokenValue(&val)) {
+ if (!val.compare(versionAttrName)) {
+ lastAttributeName = versionAttrName;
+ } else if (!val.compare(maxContentBoostAttrName)) {
+ lastAttributeName = maxContentBoostAttrName;
+ } else if (!val.compare(minContentBoostAttrName)) {
+ lastAttributeName = minContentBoostAttrName;
+ } else if (!val.compare(gammaAttrName)) {
+ lastAttributeName = gammaAttrName;
+ } else if (!val.compare(offsetSdrAttrName)) {
+ lastAttributeName = offsetSdrAttrName;
+ } else if (!val.compare(offsetHdrAttrName)) {
+ lastAttributeName = offsetHdrAttrName;
+ } else if (!val.compare(hdrCapacityMinAttrName)) {
+ lastAttributeName = hdrCapacityMinAttrName;
+ } else if (!val.compare(hdrCapacityMaxAttrName)) {
+ lastAttributeName = hdrCapacityMaxAttrName;
+ } else if (!val.compare(baseRenditionIsHdrAttrName)) {
+ lastAttributeName = baseRenditionIsHdrAttrName;
+ } else {
+ lastAttributeName = "";
+ }
+ }
+ }
+ return context.GetResult();
+ }
+
+ virtual DataMatchResult AttributeValue(const XmlTokenContext& context) {
+ string val;
+ if (state == Started) {
+ if (context.BuildTokenValue(&val, true)) {
+ if (!lastAttributeName.compare(versionAttrName)) {
+ versionStr = val;
+ versionFound = true;
+ } else if (!lastAttributeName.compare(maxContentBoostAttrName)) {
+ maxContentBoostStr = val;
+ maxContentBoostFound = true;
+ } else if (!lastAttributeName.compare(minContentBoostAttrName)) {
+ minContentBoostStr = val;
+ minContentBoostFound = true;
+ } else if (!lastAttributeName.compare(gammaAttrName)) {
+ gammaStr = val;
+ gammaFound = true;
+ } else if (!lastAttributeName.compare(offsetSdrAttrName)) {
+ offsetSdrStr = val;
+ offsetSdrFound = true;
+ } else if (!lastAttributeName.compare(offsetHdrAttrName)) {
+ offsetHdrStr = val;
+ offsetHdrFound = true;
+ } else if (!lastAttributeName.compare(hdrCapacityMinAttrName)) {
+ hdrCapacityMinStr = val;
+ hdrCapacityMinFound = true;
+ } else if (!lastAttributeName.compare(hdrCapacityMaxAttrName)) {
+ hdrCapacityMaxStr = val;
+ hdrCapacityMaxFound = true;
+ } else if (!lastAttributeName.compare(baseRenditionIsHdrAttrName)) {
+ baseRenditionIsHdrStr = val;
+ baseRenditionIsHdrFound = true;
+ }
+ }
+ }
+ return context.GetResult();
+ }
+
+ bool getVersion(string* version, bool* present) {
+ if (state == Done) {
+ *version = versionStr;
+ *present = versionFound;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool getMaxContentBoost(float* max_content_boost, bool* present) {
+ if (state == Done) {
+ *present = maxContentBoostFound;
+ stringstream ss(maxContentBoostStr);
+ float val;
+ if (ss >> val) {
+ *max_content_boost = exp2(val);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getMinContentBoost(float* min_content_boost, bool* present) {
+ if (state == Done) {
+ *present = minContentBoostFound;
+ stringstream ss(minContentBoostStr);
+ float val;
+ if (ss >> val) {
+ *min_content_boost = exp2(val);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getGamma(float* gamma, bool* present) {
+ if (state == Done) {
+ *present = gammaFound;
+ stringstream ss(gammaStr);
+ float val;
+ if (ss >> val) {
+ *gamma = val;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getOffsetSdr(float* offset_sdr, bool* present) {
+ if (state == Done) {
+ *present = offsetSdrFound;
+ stringstream ss(offsetSdrStr);
+ float val;
+ if (ss >> val) {
+ *offset_sdr = val;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getOffsetHdr(float* offset_hdr, bool* present) {
+ if (state == Done) {
+ *present = offsetHdrFound;
+ stringstream ss(offsetHdrStr);
+ float val;
+ if (ss >> val) {
+ *offset_hdr = val;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getHdrCapacityMin(float* hdr_capacity_min, bool* present) {
+ if (state == Done) {
+ *present = hdrCapacityMinFound;
+ stringstream ss(hdrCapacityMinStr);
+ float val;
+ if (ss >> val) {
+ *hdr_capacity_min = exp2(val);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getHdrCapacityMax(float* hdr_capacity_max, bool* present) {
+ if (state == Done) {
+ *present = hdrCapacityMaxFound;
+ stringstream ss(hdrCapacityMaxStr);
+ float val;
+ if (ss >> val) {
+ *hdr_capacity_max = exp2(val);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ bool getBaseRenditionIsHdr(bool* base_rendition_is_hdr, bool* present) {
+ if (state == Done) {
+ *present = baseRenditionIsHdrFound;
+ if (!baseRenditionIsHdrStr.compare("False")) {
+ *base_rendition_is_hdr = false;
+ return true;
+ } else if (!baseRenditionIsHdrStr.compare("True")) {
+ *base_rendition_is_hdr = true;
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ private:
+ static const string containerName;
+
+ static const string versionAttrName;
+ string versionStr;
+ bool versionFound;
+ static const string maxContentBoostAttrName;
+ string maxContentBoostStr;
+ bool maxContentBoostFound;
+ static const string minContentBoostAttrName;
+ string minContentBoostStr;
+ bool minContentBoostFound;
+ static const string gammaAttrName;
+ string gammaStr;
+ bool gammaFound;
+ static const string offsetSdrAttrName;
+ string offsetSdrStr;
+ bool offsetSdrFound;
+ static const string offsetHdrAttrName;
+ string offsetHdrStr;
+ bool offsetHdrFound;
+ static const string hdrCapacityMinAttrName;
+ string hdrCapacityMinStr;
+ bool hdrCapacityMinFound;
+ static const string hdrCapacityMaxAttrName;
+ string hdrCapacityMaxStr;
+ bool hdrCapacityMaxFound;
+ static const string baseRenditionIsHdrAttrName;
+ string baseRenditionIsHdrStr;
+ bool baseRenditionIsHdrFound;
+
+ string lastAttributeName;
+ ParseState state;
+};
+
+// GContainer XMP constants - URI and namespace prefix
+const string kContainerUri = "http://ns.google.com/photos/1.0/container/";
+const string kContainerPrefix = "Container";
+
+// GContainer XMP constants - element and attribute names
+const string kConDirectory = Name(kContainerPrefix, "Directory");
+const string kConItem = Name(kContainerPrefix, "Item");
+
+// GContainer XMP constants - names for XMP handlers
+const string XMPXmlHandler::containerName = "rdf:Description";
+// Item XMP constants - URI and namespace prefix
+const string kItemUri = "http://ns.google.com/photos/1.0/container/item/";
+const string kItemPrefix = "Item";
+
+// Item XMP constants - element and attribute names
+const string kItemLength = Name(kItemPrefix, "Length");
+const string kItemMime = Name(kItemPrefix, "Mime");
+const string kItemSemantic = Name(kItemPrefix, "Semantic");
+
+// Item XMP constants - element and attribute values
+const string kSemanticPrimary = "Primary";
+const string kSemanticGainMap = "GainMap";
+const string kMimeImageJpeg = "image/jpeg";
+
+// GainMap XMP constants - URI and namespace prefix
+const string kGainMapUri = "http://ns.adobe.com/hdr-gain-map/1.0/";
+const string kGainMapPrefix = "hdrgm";
+
+// GainMap XMP constants - element and attribute names
+const string kMapVersion = Name(kGainMapPrefix, "Version");
+const string kMapGainMapMin = Name(kGainMapPrefix, "GainMapMin");
+const string kMapGainMapMax = Name(kGainMapPrefix, "GainMapMax");
+const string kMapGamma = Name(kGainMapPrefix, "Gamma");
+const string kMapOffsetSdr = Name(kGainMapPrefix, "OffsetSDR");
+const string kMapOffsetHdr = Name(kGainMapPrefix, "OffsetHDR");
+const string kMapHDRCapacityMin = Name(kGainMapPrefix, "HDRCapacityMin");
+const string kMapHDRCapacityMax = Name(kGainMapPrefix, "HDRCapacityMax");
+const string kMapBaseRenditionIsHDR = Name(kGainMapPrefix, "BaseRenditionIsHDR");
+
+// GainMap XMP constants - names for XMP handlers
+const string XMPXmlHandler::versionAttrName = kMapVersion;
+const string XMPXmlHandler::minContentBoostAttrName = kMapGainMapMin;
+const string XMPXmlHandler::maxContentBoostAttrName = kMapGainMapMax;
+const string XMPXmlHandler::gammaAttrName = kMapGamma;
+const string XMPXmlHandler::offsetSdrAttrName = kMapOffsetSdr;
+const string XMPXmlHandler::offsetHdrAttrName = kMapOffsetHdr;
+const string XMPXmlHandler::hdrCapacityMinAttrName = kMapHDRCapacityMin;
+const string XMPXmlHandler::hdrCapacityMaxAttrName = kMapHDRCapacityMax;
+const string XMPXmlHandler::baseRenditionIsHdrAttrName = kMapBaseRenditionIsHDR;
+
+bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, ultrahdr_metadata_struct* metadata) {
+ string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
+
+ if (xmp_size < nameSpace.size() + 2) {
+ // Data too short
+ return false;
+ }
+
+ if (strncmp(reinterpret_cast<char*>(xmp_data), nameSpace.c_str(), nameSpace.size())) {
+ // Not correct namespace
+ return false;
+ }
+
+ // Position the pointers to the start of XMP XML portion
+ xmp_data += nameSpace.size() + 1;
+ xmp_size -= nameSpace.size() + 1;
+ XMPXmlHandler handler;
+
+ // We need to remove tail data until the closing tag. Otherwise parser will throw an error.
+ while (xmp_data[xmp_size - 1] != '>' && xmp_size > 1) {
+ xmp_size--;
+ }
+
+ string str(reinterpret_cast<const char*>(xmp_data), xmp_size);
+ MessageHandler msg_handler;
+ unique_ptr<XmlRule> rule(new XmlElementRule);
+ XmlReader reader(&handler, &msg_handler);
+ reader.StartParse(std::move(rule));
+ reader.Parse(str);
+ reader.FinishParse();
+ if (reader.HasErrors()) {
+ // Parse error
+ return false;
+ }
+
+ // Apply default values to any not-present fields, except for Version,
+ // maxContentBoost, and hdrCapacityMax, which are required. Return false if
+ // we encounter a present field that couldn't be parsed, since this
+ // indicates it is invalid (eg. string where there should be a float).
+ bool present = false;
+ if (!handler.getVersion(&metadata->version, &present) || !present) {
+ return false;
+ }
+ if (!handler.getMaxContentBoost(&metadata->maxContentBoost, &present) || !present) {
+ return false;
+ }
+ if (!handler.getHdrCapacityMax(&metadata->hdrCapacityMax, &present) || !present) {
+ return false;
+ }
+ if (!handler.getMinContentBoost(&metadata->minContentBoost, &present)) {
+ if (present) return false;
+ metadata->minContentBoost = 1.0f;
+ }
+ if (!handler.getGamma(&metadata->gamma, &present)) {
+ if (present) return false;
+ metadata->gamma = 1.0f;
+ }
+ if (!handler.getOffsetSdr(&metadata->offsetSdr, &present)) {
+ if (present) return false;
+ metadata->offsetSdr = 1.0f / 64.0f;
+ }
+ if (!handler.getOffsetHdr(&metadata->offsetHdr, &present)) {
+ if (present) return false;
+ metadata->offsetHdr = 1.0f / 64.0f;
+ }
+ if (!handler.getHdrCapacityMin(&metadata->hdrCapacityMin, &present)) {
+ if (present) return false;
+ metadata->hdrCapacityMin = 1.0f;
+ }
+
+ bool base_rendition_is_hdr;
+ if (!handler.getBaseRenditionIsHdr(&base_rendition_is_hdr, &present)) {
+ if (present) return false;
+ base_rendition_is_hdr = false;
+ }
+ if (base_rendition_is_hdr) {
+ ALOGE("Base rendition of HDR is not supported!");
+ return false;
+ }
+
+ return true;
+}
+
+string generateXmpForPrimaryImage(int secondary_image_length, ultrahdr_metadata_struct& metadata) {
+ const vector<string> kConDirSeq({kConDirectory, string("rdf:Seq")});
+ const vector<string> kLiItem({string("rdf:li"), kConItem});
+
+ std::stringstream ss;
+ photos_editing_formats::image_io::XmlWriter writer(ss);
+ writer.StartWritingElement("x:xmpmeta");
+ writer.WriteXmlns("x", "adobe:ns:meta/");
+ writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2");
+ writer.StartWritingElement("rdf:RDF");
+ writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
+ writer.StartWritingElement("rdf:Description");
+ writer.WriteXmlns(kContainerPrefix, kContainerUri);
+ writer.WriteXmlns(kItemPrefix, kItemUri);
+ writer.WriteXmlns(kGainMapPrefix, kGainMapUri);
+ writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
+
+ writer.StartWritingElements(kConDirSeq);
+
+ size_t item_depth = writer.StartWritingElement("rdf:li");
+ writer.WriteAttributeNameAndValue("rdf:parseType", "Resource");
+ writer.StartWritingElement(kConItem);
+ writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary);
+ writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
+ writer.FinishWritingElementsToDepth(item_depth);
+
+ writer.StartWritingElement("rdf:li");
+ writer.WriteAttributeNameAndValue("rdf:parseType", "Resource");
+ writer.StartWritingElement(kConItem);
+ writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticGainMap);
+ writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
+ writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length);
+
+ writer.FinishWriting();
+
+ return ss.str();
+}
+
+string generateXmpForSecondaryImage(ultrahdr_metadata_struct& metadata) {
+ const vector<string> kConDirSeq({kConDirectory, string("rdf:Seq")});
+
+ std::stringstream ss;
+ photos_editing_formats::image_io::XmlWriter writer(ss);
+ writer.StartWritingElement("x:xmpmeta");
+ writer.WriteXmlns("x", "adobe:ns:meta/");
+ writer.WriteAttributeNameAndValue("x:xmptk", "Adobe XMP Core 5.1.2");
+ writer.StartWritingElement("rdf:RDF");
+ writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
+ writer.StartWritingElement("rdf:Description");
+ writer.WriteXmlns(kGainMapPrefix, kGainMapUri);
+ writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
+ writer.WriteAttributeNameAndValue(kMapGainMapMin, log2(metadata.minContentBoost));
+ writer.WriteAttributeNameAndValue(kMapGainMapMax, log2(metadata.maxContentBoost));
+ writer.WriteAttributeNameAndValue(kMapGamma, metadata.gamma);
+ writer.WriteAttributeNameAndValue(kMapOffsetSdr, metadata.offsetSdr);
+ writer.WriteAttributeNameAndValue(kMapOffsetHdr, metadata.offsetHdr);
+ writer.WriteAttributeNameAndValue(kMapHDRCapacityMin, log2(metadata.hdrCapacityMin));
+ writer.WriteAttributeNameAndValue(kMapHDRCapacityMax, log2(metadata.hdrCapacityMax));
+ writer.WriteAttributeNameAndValue(kMapBaseRenditionIsHDR, "False");
+ writer.FinishWriting();
+
+ return ss.str();
+}
+
+} // namespace ultrahdr
diff --git a/lib/src/multipictureformat.cpp b/lib/src/multipictureformat.cpp
new file mode 100644
index 0000000..59efc66
--- /dev/null
+++ b/lib/src/multipictureformat.cpp
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "ultrahdr/multipictureformat.h"
+
+namespace ultrahdr {
+size_t calculateMpfSize() {
+ return sizeof(kMpfSig) + // Signature
+ kMpEndianSize + // Endianness
+ sizeof(uint32_t) + // Index IFD Offset
+ sizeof(uint16_t) + // Tag count
+ kTagSerializedCount * kTagSize + // 3 tags at 12 bytes each
+ sizeof(uint32_t) + // Attribute IFD offset
+ kNumPictures * kMPEntrySize; // MP Entries for each image
+}
+
+std::shared_ptr<DataStruct> generateMpf(int primary_image_size, int primary_image_offset,
+ int secondary_image_size, int secondary_image_offset) {
+ size_t mpf_size = calculateMpfSize();
+ std::shared_ptr<DataStruct> dataStruct = std::make_shared<DataStruct>(mpf_size);
+
+ dataStruct->write(static_cast<const void*>(kMpfSig), sizeof(kMpfSig));
+#if USE_BIG_ENDIAN_IN_MPF
+ dataStruct->write(static_cast<const void*>(kMpBigEndian), kMpEndianSize);
+#else
+ dataStruct->write(static_cast<const void*>(kMpLittleEndian), kMpEndianSize);
+#endif
+
+ // Set the Index IFD offset be the position after the endianness value and this offset.
+ constexpr uint32_t indexIfdOffset = static_cast<uint16_t>(kMpEndianSize + sizeof(kMpfSig));
+ dataStruct->write32(Endian_SwapBE32(indexIfdOffset));
+
+ // We will write 3 tags (version, number of images, MP entries).
+ dataStruct->write16(Endian_SwapBE16(kTagSerializedCount));
+
+ // Write the version tag.
+ dataStruct->write16(Endian_SwapBE16(kVersionTag));
+ dataStruct->write16(Endian_SwapBE16(kVersionType));
+ dataStruct->write32(Endian_SwapBE32(kVersionCount));
+ dataStruct->write(kVersionExpected, kVersionSize);
+
+ // Write the number of images.
+ dataStruct->write16(Endian_SwapBE16(kNumberOfImagesTag));
+ dataStruct->write16(Endian_SwapBE16(kNumberOfImagesType));
+ dataStruct->write32(Endian_SwapBE32(kNumberOfImagesCount));
+ dataStruct->write32(Endian_SwapBE32(kNumPictures));
+
+ // Write the MP entries.
+ dataStruct->write16(Endian_SwapBE16(kMPEntryTag));
+ dataStruct->write16(Endian_SwapBE16(kMPEntryType));
+ dataStruct->write32(Endian_SwapBE32(kMPEntrySize * kNumPictures));
+ const uint32_t mpEntryOffset =
+ static_cast<uint32_t>(dataStruct->getBytesWritten() - // The bytes written so far
+ sizeof(kMpfSig) + // Excluding the MPF signature
+ sizeof(uint32_t) + // The 4 bytes for this offset
+ sizeof(uint32_t)); // The 4 bytes for the attribute IFD offset.
+ dataStruct->write32(Endian_SwapBE32(mpEntryOffset));
+
+ // Write the attribute IFD offset (zero because we don't write it).
+ dataStruct->write32(0);
+
+ // Write the MP entries for primary image
+ dataStruct->write32(Endian_SwapBE32(kMPEntryAttributeFormatJpeg | kMPEntryAttributeTypePrimary));
+ dataStruct->write32(Endian_SwapBE32(primary_image_size));
+ dataStruct->write32(Endian_SwapBE32(primary_image_offset));
+ dataStruct->write16(0);
+ dataStruct->write16(0);
+
+ // Write the MP entries for secondary image
+ dataStruct->write32(Endian_SwapBE32(kMPEntryAttributeFormatJpeg));
+ dataStruct->write32(Endian_SwapBE32(secondary_image_size));
+ dataStruct->write32(Endian_SwapBE32(secondary_image_offset));
+ dataStruct->write16(0);
+ dataStruct->write16(0);
+
+ return dataStruct;
+}
+
+} // namespace ultrahdr