1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5 
6 #include "lib/jxl/enc_file.h"
7 
8 #include <stddef.h>
9 
10 #include <type_traits>
11 #include <utility>
12 #include <vector>
13 
14 #include "lib/jxl/aux_out.h"
15 #include "lib/jxl/aux_out_fwd.h"
16 #include "lib/jxl/base/compiler_specific.h"
17 #include "lib/jxl/codec_in_out.h"
18 #include "lib/jxl/color_encoding_internal.h"
19 #include "lib/jxl/enc_bit_writer.h"
20 #include "lib/jxl/enc_cache.h"
21 #include "lib/jxl/enc_frame.h"
22 #include "lib/jxl/enc_icc_codec.h"
23 #include "lib/jxl/frame_header.h"
24 #include "lib/jxl/headers.h"
25 #include "lib/jxl/image_bundle.h"
26 
27 namespace jxl {
28 
29 namespace {
30 
31 // DC + 'Very Low Frequency'
32 PassDefinition progressive_passes_dc_vlf[] = {
33     {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false,
34      /*suitable_for_downsampling_of_at_least=*/4}};
35 
36 PassDefinition progressive_passes_dc_lf[] = {
37     {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false,
38      /*suitable_for_downsampling_of_at_least=*/4},
39     {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false,
40      /*suitable_for_downsampling_of_at_least=*/2}};
41 
42 PassDefinition progressive_passes_dc_lf_salient_ac[] = {
43     {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false,
44      /*suitable_for_downsampling_of_at_least=*/4},
45     {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false,
46      /*suitable_for_downsampling_of_at_least=*/2},
47     {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/true,
48      /*suitable_for_downsampling_of_at_least=*/0}};
49 
50 PassDefinition progressive_passes_dc_lf_salient_ac_other_ac[] = {
51     {/*num_coefficients=*/2, /*shift=*/0, /*salient_only=*/false,
52      /*suitable_for_downsampling_of_at_least=*/4},
53     {/*num_coefficients=*/3, /*shift=*/0, /*salient_only=*/false,
54      /*suitable_for_downsampling_of_at_least=*/2},
55     {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/true,
56      /*suitable_for_downsampling_of_at_least=*/0},
57     {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false,
58      /*suitable_for_downsampling_of_at_least=*/0}};
59 
60 PassDefinition progressive_passes_dc_quant_ac_full_ac[] = {
61     {/*num_coefficients=*/8, /*shift=*/1, /*salient_only=*/false,
62      /*suitable_for_downsampling_of_at_least=*/2},
63     {/*num_coefficients=*/8, /*shift=*/0, /*salient_only=*/false,
64      /*suitable_for_downsampling_of_at_least=*/0},
65 };
66 
67 constexpr uint16_t kExifOrientationTag = 274;
68 
69 // Parses the Exif data just enough to extract any render-impacting info.
70 // If the Exif data is invalid or could not be parsed, then it is treated
71 // as a no-op.
72 // TODO (jon): tag 1 can be used to represent Adobe RGB 1998 if it has value
73 // "R03"
74 // TODO (jon): set intrinsic dimensions according to
75 // https://discourse.wicg.io/t/proposal-exif-image-resolution-auto-and-from-image/4326/24
InterpretExif(const PaddedBytes & exif,CodecMetadata * metadata)76 void InterpretExif(const PaddedBytes& exif, CodecMetadata* metadata) {
77   if (exif.size() < 12) return;  // not enough bytes for a valid exif blob
78   const uint8_t* t = exif.data();
79   bool bigendian = false;
80   if (LoadLE32(t) == 0x2A004D4D) {
81     bigendian = true;
82   } else if (LoadLE32(t) != 0x002A4949) {
83     return;  // not a valid tiff header
84   }
85   t += 4;
86   uint32_t offset = (bigendian ? LoadBE32(t) : LoadLE32(t));
87   if (exif.size() < 12 + offset + 2 || offset < 8) return;
88   t += offset - 4;
89   uint16_t nb_tags = (bigendian ? LoadBE16(t) : LoadLE16(t));
90   t += 2;
91   while (nb_tags > 0) {
92     if (t + 12 >= exif.data() + exif.size()) return;
93     uint16_t tag = (bigendian ? LoadBE16(t) : LoadLE16(t));
94     t += 2;
95     uint16_t type = (bigendian ? LoadBE16(t) : LoadLE16(t));
96     t += 2;
97     uint32_t count = (bigendian ? LoadBE32(t) : LoadLE32(t));
98     t += 4;
99     uint16_t value = (bigendian ? LoadBE16(t) : LoadLE16(t));
100     t += 4;
101     if (tag == kExifOrientationTag) {
102       if (type == 3 && count == 1) {
103         if (value >= 1 && value <= 8) {
104           metadata->m.orientation = value;
105         }
106       }
107     }
108     nb_tags--;
109   }
110 }
111 
PrepareCodecMetadataFromIO(const CompressParams & cparams,const CodecInOut * io,CodecMetadata * metadata)112 Status PrepareCodecMetadataFromIO(const CompressParams& cparams,
113                                   const CodecInOut* io,
114                                   CodecMetadata* metadata) {
115   *metadata = io->metadata;
116   size_t ups = 1;
117   if (cparams.already_downsampled) ups = cparams.resampling;
118 
119   JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize() * ups, io->ysize() * ups));
120 
121   // Keep ICC profile in lossless modes because a reconstructed profile may be
122   // slightly different (quantization).
123   // Also keep ICC in JPEG reconstruction mode as we need byte-exact profiles.
124   const bool lossless_modular =
125       cparams.modular_mode && cparams.quality_pair.first == 100.0f;
126   if (!lossless_modular && !io->Main().IsJPEG()) {
127     metadata->m.color_encoding.DecideIfWantICC();
128   }
129 
130   metadata->m.xyb_encoded =
131       cparams.color_transform == ColorTransform::kXYB ? true : false;
132 
133   InterpretExif(io->blobs.exif, metadata);
134 
135   return true;
136 }
137 
138 }  // namespace
139 
EncodePreview(const CompressParams & cparams,const ImageBundle & ib,const CodecMetadata * metadata,const JxlCmsInterface & cms,ThreadPool * pool,BitWriter * JXL_RESTRICT writer)140 Status EncodePreview(const CompressParams& cparams, const ImageBundle& ib,
141                      const CodecMetadata* metadata, const JxlCmsInterface& cms,
142                      ThreadPool* pool, BitWriter* JXL_RESTRICT writer) {
143   BitWriter preview_writer;
144   // TODO(janwas): also support generating preview by downsampling
145   if (ib.HasColor()) {
146     AuxOut aux_out;
147     PassesEncoderState passes_enc_state;
148     // TODO(lode): check if we want all extra channels and matching xyb_encoded
149     // for the preview, such that using the main ImageMetadata object for
150     // encoding this frame is warrented.
151     FrameInfo frame_info;
152     frame_info.is_preview = true;
153     JXL_RETURN_IF_ERROR(EncodeFrame(cparams, frame_info, metadata, ib,
154                                     &passes_enc_state, cms, pool,
155                                     &preview_writer, &aux_out));
156     preview_writer.ZeroPadToByte();
157   }
158 
159   if (preview_writer.BitsWritten() != 0) {
160     writer->ZeroPadToByte();
161     writer->AppendByteAligned(preview_writer);
162   }
163 
164   return true;
165 }
166 
WriteHeaders(CodecMetadata * metadata,BitWriter * writer,AuxOut * aux_out)167 Status WriteHeaders(CodecMetadata* metadata, BitWriter* writer,
168                     AuxOut* aux_out) {
169   // Marker/signature
170   BitWriter::Allotment allotment(writer, 16);
171   writer->Write(8, 0xFF);
172   writer->Write(8, kCodestreamMarker);
173   ReclaimAndCharge(writer, &allotment, kLayerHeader, aux_out);
174 
175   JXL_RETURN_IF_ERROR(
176       WriteSizeHeader(metadata->size, writer, kLayerHeader, aux_out));
177 
178   JXL_RETURN_IF_ERROR(
179       WriteImageMetadata(metadata->m, writer, kLayerHeader, aux_out));
180 
181   metadata->transform_data.nonserialized_xyb_encoded = metadata->m.xyb_encoded;
182   JXL_RETURN_IF_ERROR(
183       Bundle::Write(metadata->transform_data, writer, kLayerHeader, aux_out));
184 
185   return true;
186 }
187 
EncodeFile(const CompressParams & cparams_orig,const CodecInOut * io,PassesEncoderState * passes_enc_state,PaddedBytes * compressed,const JxlCmsInterface & cms,AuxOut * aux_out,ThreadPool * pool)188 Status EncodeFile(const CompressParams& cparams_orig, const CodecInOut* io,
189                   PassesEncoderState* passes_enc_state, PaddedBytes* compressed,
190                   const JxlCmsInterface& cms, AuxOut* aux_out,
191                   ThreadPool* pool) {
192   io->CheckMetadata();
193   BitWriter writer;
194 
195   CompressParams cparams = cparams_orig;
196   if (io->Main().color_transform != ColorTransform::kNone) {
197     // Set the color transform to YCbCr or XYB if the original image is such.
198     cparams.color_transform = io->Main().color_transform;
199   }
200 
201   // TODO(lode): move this to a common CompressParam post-initializer that is
202   // mandatory to be called, so that the encode API can also use it, once the
203   // encode API has the settings for resampling in the first place.
204   if (cparams.resampling == 0) {
205     cparams.resampling = 1;
206     // For very low bit rates, using 2x2 resampling gives better results on
207     // most photographic images, with an adjusted butteraugli score chosen to
208     // give roughly the same amount of bits per pixel.
209     if (!cparams.already_downsampled && cparams.butteraugli_distance >= 20) {
210       cparams.resampling = 2;
211       cparams.butteraugli_distance =
212           6 + ((cparams.butteraugli_distance - 20) * 0.25);
213     }
214   }
215 
216   std::unique_ptr<CodecMetadata> metadata = jxl::make_unique<CodecMetadata>();
217   JXL_RETURN_IF_ERROR(PrepareCodecMetadataFromIO(cparams, io, metadata.get()));
218   JXL_RETURN_IF_ERROR(WriteHeaders(metadata.get(), &writer, aux_out));
219 
220   // Only send ICC (at least several hundred bytes) if fields aren't enough.
221   if (metadata->m.color_encoding.WantICC()) {
222     JXL_RETURN_IF_ERROR(WriteICC(metadata->m.color_encoding.ICC(), &writer,
223                                  kLayerHeader, aux_out));
224   }
225 
226   if (metadata->m.have_preview) {
227     JXL_RETURN_IF_ERROR(EncodePreview(cparams, io->preview_frame,
228                                       metadata.get(), cms, pool, &writer));
229   }
230 
231   // Each frame should start on byte boundaries.
232   writer.ZeroPadToByte();
233 
234   if (cparams.progressive_mode || cparams.qprogressive_mode) {
235     if (cparams.saliency_map != nullptr) {
236       passes_enc_state->progressive_splitter.SetSaliencyMap(
237           cparams.saliency_map);
238     }
239     passes_enc_state->progressive_splitter.SetSaliencyThreshold(
240         cparams.saliency_threshold);
241     if (cparams.qprogressive_mode) {
242       passes_enc_state->progressive_splitter.SetProgressiveMode(
243           ProgressiveMode{progressive_passes_dc_quant_ac_full_ac});
244     } else {
245       switch (cparams.saliency_num_progressive_steps) {
246         case 1:
247           passes_enc_state->progressive_splitter.SetProgressiveMode(
248               ProgressiveMode{progressive_passes_dc_vlf});
249           break;
250         case 2:
251           passes_enc_state->progressive_splitter.SetProgressiveMode(
252               ProgressiveMode{progressive_passes_dc_lf});
253           break;
254         case 3:
255           passes_enc_state->progressive_splitter.SetProgressiveMode(
256               ProgressiveMode{progressive_passes_dc_lf_salient_ac});
257           break;
258         case 4:
259           if (cparams.saliency_threshold == 0.0f) {
260             // No need for a 4th pass if saliency-threshold regards everything
261             // as salient.
262             passes_enc_state->progressive_splitter.SetProgressiveMode(
263                 ProgressiveMode{progressive_passes_dc_lf_salient_ac});
264           } else {
265             passes_enc_state->progressive_splitter.SetProgressiveMode(
266                 ProgressiveMode{progressive_passes_dc_lf_salient_ac_other_ac});
267           }
268           break;
269         default:
270           return JXL_FAILURE("Invalid saliency_num_progressive_steps.");
271       }
272     }
273   }
274 
275   for (size_t i = 0; i < io->frames.size(); i++) {
276     FrameInfo info;
277     info.is_last = i == io->frames.size() - 1;
278     if (io->frames[i].use_for_next_frame) {
279       info.save_as_reference = 1;
280     }
281     JXL_RETURN_IF_ERROR(EncodeFrame(cparams, info, metadata.get(),
282                                     io->frames[i], passes_enc_state, cms, pool,
283                                     &writer, aux_out));
284   }
285 
286   // Clean up passes_enc_state in case it gets reused.
287   for (size_t i = 0; i < 4; i++) {
288     passes_enc_state->shared.dc_frames[i] = Image3F();
289     passes_enc_state->shared.reference_frames[i].storage = ImageBundle();
290   }
291 
292   *compressed = std::move(writer).TakeBytes();
293   return true;
294 }
295 
296 }  // namespace jxl
297