Coverage Report

Created: 2026-01-21 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/piex/src/piex_cr3.cc
Line
Count
Source
1
// Copyright 2020 Google Inc.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
//
15
////////////////////////////////////////////////////////////////////////////////
16
17
#include "src/piex_cr3.h"
18
19
#include <array>
20
#include <cstddef>
21
#include <cstdint>
22
#include <limits>
23
#include <unordered_set>
24
25
#include "src/binary_parse/range_checked_byte_ptr.h"
26
#include "src/piex_types.h"
27
#include "src/tiff_directory/tiff_directory.h"
28
#include "src/tiff_parser.h"
29
30
namespace piex {
31
namespace {
32
33
constexpr size_t kUuidSize = 16;
34
using Uuid = std::array<std::uint8_t, kUuidSize>;
35
// Uuid of uuid box under the moov box.
36
constexpr Uuid kUuidMoov = {0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0,
37
                            0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48};
38
39
// Uuid of uuid box containing PRVW box.
40
constexpr Uuid kUuidPrvw = {0xea, 0xf4, 0x2b, 0x5e, 0x1c, 0x98, 0x4b, 0x88,
41
                            0xb9, 0xfb, 0xb7, 0xdc, 0x40, 0x6e, 0x4d, 0x16};
42
43
constexpr size_t kTagSize = 4;
44
using BoxTag = std::array<char, kTagSize>;
45
46
4
constexpr BoxTag NewTag(const char s[kTagSize + 1]) {
47
4
  return BoxTag{s[0], s[1], s[2], s[3]};
48
4
}
49
50
constexpr BoxTag kUuidTag = NewTag("uuid");
51
constexpr BoxTag kPrvwTag = NewTag("PRVW");
52
constexpr BoxTag kThmbTag = NewTag("THMB");
53
constexpr BoxTag kCmt1Tag = NewTag("CMT1");
54
constexpr BoxTag kCmt2Tag = NewTag("CMT2");
55
constexpr BoxTag kStblTag = NewTag("stbl");
56
constexpr BoxTag kStsdTag = NewTag("stsd");
57
constexpr BoxTag kCrawTag = NewTag("CRAW");
58
constexpr BoxTag kStszTag = NewTag("stsz");
59
constexpr BoxTag kCo64Tag = NewTag("co64");
60
constexpr BoxTag kMdatTag = NewTag("mdat");
61
62
// Convenience class for a box.
63
class Box {
64
 public:
65
  Box()
66
358
      : is_valid_(false),
67
358
        tag_(BoxTag()),
68
358
        offset_(0),
69
358
        header_offset_(0),
70
358
        next_box_offset_(0) {}
71
  Box(const BoxTag& tag, size_t offset, size_t header_length, size_t length)
72
9.12k
      : is_valid_(true),
73
9.12k
        tag_(tag),
74
9.12k
        offset_(offset),
75
9.12k
        header_offset_(offset + header_length),
76
9.12k
        next_box_offset_(offset + length) {}
77
78
10.6k
  bool IsValid() const { return is_valid_ && next_box_offset_ > offset_; }
79
47.7k
  const BoxTag& tag() const { return tag_; }
80
81
  // Returns offset from start of file.
82
3.39k
  size_t offset() const { return offset_; }
83
  // Returns offset from start of file, including box's header.
84
5.36k
  size_t header_offset() const { return header_offset_; }
85
  // Returns offset from start of file of the next box, accounting for size of
86
  // this box.
87
6.53k
  size_t next_box_offset() const { return next_box_offset_; }
88
89
 private:
90
  bool is_valid_;
91
  BoxTag tag_;
92
  size_t offset_;
93
  size_t header_offset_;
94
  size_t next_box_offset_;
95
};
96
97
struct ProcessData {
98
  PreviewImageData* preview_image_data = nullptr;
99
  Image mdat_image;
100
  Image prvw_image;
101
};
102
103
// Wraps Get16u w/ assumption that CR3 is always big endian, based on
104
// ISO/IEC 14496-12 specification that all box fields are big endian.
105
2.14k
bool Get16u(StreamInterface* stream, size_t offset, std::uint16_t* value) {
106
2.14k
  return Get16u(stream, offset, tiff_directory::kBigEndian, value);
107
2.14k
}
108
109
// Wraps Get32u w/ assumption that CR3 is always big endian, based on
110
// ISO/IEC 14496-12 specification that all box fields are big endian.
111
10.5k
bool Get32u(StreamInterface* stream, size_t offset, std::uint32_t* value) {
112
10.5k
  return Get32u(stream, offset, tiff_directory::kBigEndian, value);
113
10.5k
}
114
115
// Always big endian, based on ISO/IEC 14496-12 specification that all box
116
// fields are big endian.
117
361
bool Get64u(StreamInterface* stream, size_t offset, std::uint64_t* value) {
118
361
  std::uint8_t data[8];
119
361
  if (stream->GetData(offset, 8, data) == kOk) {
120
360
    *value = (data[0] * 0x1000000u) | (data[1] * 0x10000u) |
121
360
             (data[2] * 0x100u) | data[3];
122
360
    *value <<= 32;
123
360
    *value = (data[4] * 0x1000000u) | (data[5] * 0x10000u) |
124
360
             (data[6] * 0x100u) | data[7];
125
360
    return true;
126
360
  } else {
127
1
    return false;
128
1
  }
129
361
}
130
131
// Jpeg box offsets based on the box tag. The expected layout is as follows:
132
//        Byte Offset Type     Meaning
133
//                  0 [long]   size of box
134
//                  4 [char[]] box tag
135
//       offset.width [short]  width of jpeg
136
//      offset.height [short]  height of jpeg
137
//   offset.jpeg_size [long]   number of bytes in jpeg
138
//   offset.jpeg_data [byte[]] start of jpeg data
139
struct JpegBoxOffset {
140
  size_t width = 0;
141
  size_t height = 0;
142
  size_t jpeg_size = 0;
143
  size_t jpeg_data = 0;
144
};
145
146
// Processes box w/ JPEG data. Box must be PRVW and THMB boxes.
147
428
bool ProcessJpegBox(StreamInterface* stream, const Box& box, Image* image) {
148
428
  static constexpr JpegBoxOffset kPrvwJpegOffsets{14, 16, 20, 24};
149
428
  static constexpr JpegBoxOffset kThmbJpegOffsets{12, 14, 16, 24};
150
428
  if (box.tag() != kPrvwTag && box.tag() != kThmbTag) {
151
0
    return false;
152
0
  }
153
428
  const JpegBoxOffset& offsets =
154
428
      box.tag() == kPrvwTag ? kPrvwJpegOffsets : kThmbJpegOffsets;
155
428
  uint16_t width, height;
156
428
  uint32_t jpeg_size;
157
428
  if (!Get16u(stream, box.offset() + offsets.width, &width)) {
158
2
    return false;
159
2
  }
160
426
  if (!Get16u(stream, box.offset() + offsets.height, &height)) {
161
1
    return false;
162
1
  }
163
425
  if (!Get32u(stream, box.offset() + offsets.jpeg_size, &jpeg_size)) {
164
1
    return false;
165
1
  }
166
424
  image->format = Image::kJpegCompressed;
167
424
  image->width = width;
168
424
  image->height = height;
169
424
  image->offset = box.offset() + offsets.jpeg_data;
170
424
  image->length = jpeg_size;
171
424
  return true;
172
425
}
173
174
// Parses the Exif IFD0 tags at tiff_offset.
175
bool ParseExifIfd0(StreamInterface* stream, size_t tiff_offset,
176
346
                   PreviewImageData* preview_image_data) {
177
346
  static const TagSet kIfd0TagSet = {kTiffTagModel, kTiffTagMake,
178
346
                                     kTiffTagOrientation, kTiffTagImageWidth,
179
346
                                     kTiffTagImageLength};
180
346
  TiffContent content;
181
346
  TiffParser(stream, tiff_offset).Parse(kIfd0TagSet, 1, &content);
182
346
  if (content.tiff_directory.size() != 1) {
183
16
    return false;
184
16
  }
185
186
330
  content.tiff_directory[0].Get(kTiffTagModel, &preview_image_data->model);
187
330
  content.tiff_directory[0].Get(kTiffTagMake, &preview_image_data->maker);
188
330
  content.tiff_directory[0].Get(kTiffTagOrientation,
189
330
                                &preview_image_data->exif_orientation);
190
330
  content.tiff_directory[0].Get(kTiffTagImageWidth,
191
330
                                &preview_image_data->full_width);
192
330
  content.tiff_directory[0].Get(kTiffTagImageLength,
193
330
                                &preview_image_data->full_height);
194
330
  return true;
195
346
}
196
197
// Parses the Exif Exif IFD tags at tiff_offset.
198
bool ParseExifExifIfd(StreamInterface* stream, size_t tiff_offset,
199
872
                      PreviewImageData* preview_image_data) {
200
872
  static const TagSet kExifIfdTagSet = {kExifTagDateTimeOriginal,
201
872
                                        kExifTagExposureTime, kExifTagFnumber,
202
872
                                        kExifTagFocalLength, kExifTagIsoSpeed};
203
872
  TiffContent content;
204
872
  TiffParser(stream, tiff_offset).Parse(kExifIfdTagSet, 1, &content);
205
872
  if (content.tiff_directory.size() != 1) {
206
48
    return false;
207
48
  }
208
209
824
  content.tiff_directory[0].Get(kExifTagDateTimeOriginal,
210
824
                                &preview_image_data->date_time);
211
824
  GetRational(kExifTagExposureTime, content.tiff_directory[0], 1,
212
824
              &preview_image_data->exposure_time);
213
824
  GetRational(kExifTagFnumber, content.tiff_directory[0], 1,
214
824
              &preview_image_data->fnumber);
215
824
  GetRational(kExifTagFocalLength, content.tiff_directory[0], 1,
216
824
              &preview_image_data->focal_length);
217
824
  content.tiff_directory[0].Get(kExifTagIsoSpeed, &preview_image_data->iso);
218
824
  return true;
219
872
}
220
221
// Returns the next box or an invalid box.
222
//
223
// Based on ISO/IEC 14496-12: boxes start with a header: size and type. The size
224
// can be compact (32-bits) or extended (64-bit, e.g. mdat box).
225
// The type can be compact (32 bits) or extended (full UUID, e.g. uuid boxes).
226
// values are stored after the compact size/type.
227
//
228
// Fields in a box are big-endian.
229
9.48k
Box GetNextBox(StreamInterface* stream, size_t offset) {
230
9.48k
  uint32_t length_32;
231
9.48k
  if (!Get32u(stream, offset, &length_32)) {
232
338
    return Box();
233
338
  }
234
9.14k
  BoxTag tag;
235
9.14k
  Error status = stream->GetData(offset + sizeof(length_32), kTagSize,
236
9.14k
                                 reinterpret_cast<std::uint8_t*>(tag.data()));
237
9.14k
  if (status != kOk) {
238
19
    return Box();
239
19
  }
240
9.13k
  size_t length;
241
9.13k
  size_t header_offset = sizeof(length_32) + sizeof(tag);
242
9.13k
  if (length_32 == 1) {
243
    // Magic number of 1 implies extended size.
244
361
    uint64_t length_64 = 0;
245
361
    if (!Get64u(stream, offset + header_offset, &length_64)) {
246
1
      return Box();
247
1
    }
248
360
    length = length_64;
249
360
    header_offset += sizeof(length_64);
250
8.76k
  } else {
251
    // Compact size.
252
8.76k
    length = length_32;
253
8.76k
  }
254
9.12k
  return Box(tag, offset, header_offset, length);
255
9.13k
}
256
257
// Searches for the next box with the given tag.
258
Box GetNextBoxWithTag(StreamInterface* stream, size_t offset,
259
1.14k
                      const BoxTag& expected_tag) {
260
2.25k
  while (true) {
261
2.25k
    Box box = GetNextBox(stream, offset);
262
2.25k
    if (!box.IsValid() || box.tag() == expected_tag) {
263
1.14k
      return box;
264
1.14k
    }
265
1.11k
    offset = box.next_box_offset();
266
1.11k
  }
267
1.14k
}
268
269
// Returns the width, height, and content type from the CRAW box.
270
bool ProcessCrawBox(StreamInterface* stream, const Box& craw_box,
271
430
                    uint16_t* width, uint16_t* height, uint16_t* content_type) {
272
430
  constexpr size_t kWidthOffset = 32;
273
430
  if (!Get16u(stream, craw_box.offset() + kWidthOffset, width)) {
274
1
    return false;
275
1
  }
276
277
429
  constexpr size_t kHeightOffset = 34;
278
429
  if (!Get16u(stream, craw_box.offset() + kHeightOffset, height)) {
279
1
    return false;
280
1
  }
281
282
428
  constexpr size_t kTypeOffset = 86;
283
428
  if (!Get16u(stream, craw_box.offset() + kTypeOffset, content_type)) {
284
2
    return false;
285
2
  }
286
426
  return true;
287
428
}
288
289
// stsz box offset:
290
//        Byte Offset Type     Meaning
291
//                  0 [long]   size of box
292
//                  4 [char[]] box tag
293
//                  8 [long]   version/flags
294
//                 12 [long]   sample size
295
//                 16 [long]   number of entries in sample table
296
//                 20 [long[]] sample table if samples size is 0
297
bool ProcessStszBox(StreamInterface* stream, const Box& stsz_box,
298
297
                    uint32_t* image_size) {
299
297
  uint32_t sample_size;
300
297
  if (!Get32u(stream, stsz_box.offset() + 12, &sample_size)) {
301
2
    return false;
302
2
  }
303
295
  if (sample_size > 0) {
304
217
    *image_size = sample_size;
305
217
    return true;
306
217
  }
307
  // sample_size of 0 implies the data is in the sample table. We expect only
308
  // one entry. This is true of Canon EOS RP Cr3 files.
309
78
  uint32_t count;
310
78
  if (!Get32u(stream, stsz_box.offset() + 16, &count)) {
311
1
    return false;
312
1
  }
313
77
  if (count != 1) {
314
    // Expect at most one entry in the table.
315
48
    return false;
316
48
  }
317
29
  return Get32u(stream, stsz_box.offset() + 20, image_size);
318
77
}
319
320
// co64 box offsets:
321
//        Byte Offset Type     Meaning
322
//                  0 [long]   size of box
323
//                  4 [char[]] box tag
324
//                  8 [long]   version
325
//                 12 [long]   count (expect to be value 1)
326
//                 16 [long]   offset of image data in mdat
327
bool ProcessCo64(StreamInterface* stream, const Box& co64_box,
328
178
                 uint32_t* image_offset) {
329
178
  uint32_t count = 0;
330
178
  if (!Get32u(stream, co64_box.header_offset() + 4, &count)) {
331
1
    return false;
332
1
  }
333
177
  if (count != 1) {
334
72
    return false;
335
72
  }
336
105
  return Get32u(stream, co64_box.header_offset() + 8, image_offset);
337
177
}
338
339
// Process the stbl box. Expected box layout:
340
// stbl
341
//   stsd
342
//     CRAW  (embedded image (JPEG) information)
343
//   (0 or more skipped boxes)
344
//   stsz (embedded image byte size)
345
//   (0 or more skipped boxes)
346
//   co64 (offset of embedded image, relative to mdat box)
347
bool ProcessStblBox(StreamInterface* stream, const Box& stbl_box,
348
566
                    ProcessData* data) {
349
566
  Box stsd_box = GetNextBoxWithTag(stream, stbl_box.header_offset(), kStsdTag);
350
566
  if (!stsd_box.IsValid()) {
351
20
    return false;
352
20
  }
353
  // This is either CRAW or CTMD. Skip when CTMD.
354
546
  Box craw_box = GetNextBox(stream, stsd_box.header_offset() + 8);
355
546
  if (!craw_box.IsValid()) {
356
3
    return false;
357
3
  }
358
543
  if (craw_box.tag() != kCrawTag) {
359
113
    return true;
360
113
  }
361
  // CRAW contains info about the full-size image embedded in the mdat box.
362
  // The image is either JPEG or HEVC.
363
430
  uint16_t image_width = 0;
364
430
  uint16_t image_height = 0;
365
430
  uint16_t content_type = 0;
366
430
  if (!ProcessCrawBox(stream, craw_box, &image_width, &image_height,
367
430
                      &content_type)) {
368
4
    return false;
369
4
  }
370
  // Only continue if JPEG or HEVC content.
371
426
  constexpr uint16_t kJpegContentType = 3;
372
426
  constexpr uint16_t kHevcContentType = 4;
373
426
  if (content_type != kJpegContentType && content_type != kHevcContentType) {
374
96
    return true;
375
96
  }
376
377
  // Skip until we find stsz, contains the size (# of bytes) of image data.
378
330
  Box stsz_box =
379
330
      GetNextBoxWithTag(stream, stsd_box.next_box_offset(), kStszTag);
380
330
  if (!stsz_box.IsValid()) {
381
33
    return false;
382
33
  }
383
297
  uint32_t image_size;
384
297
  if (!ProcessStszBox(stream, stsz_box, &image_size)) {
385
51
    return false;
386
51
  }
387
388
  // Skip until we find co64, contains the offset of image data.
389
246
  Box co64_box =
390
246
      GetNextBoxWithTag(stream, stsz_box.next_box_offset(), kCo64Tag);
391
246
  if (!co64_box.IsValid()) {
392
68
    return false;
393
68
  }
394
395
178
  uint32_t image_offset = 0;
396
178
  if (!ProcessCo64(stream, co64_box, &image_offset)) {
397
73
    return false;
398
73
  }
399
400
105
  data->mdat_image.format = content_type == kJpegContentType
401
105
                                ? Image::kJpegCompressed
402
105
                                : Image::kHevcCompressed;
403
105
  data->mdat_image.width = image_width;
404
105
  data->mdat_image.height = image_height;
405
105
  data->mdat_image.length = image_size;
406
  // This offset is relative to the position of the mdat box. The value will
407
  // be updated once mdat's offset is found.
408
105
  data->mdat_image.offset = image_offset;
409
105
  return true;
410
178
}
411
412
// Returns true if we should parse the children of the box.
413
5.05k
bool DoProcessChildren(const BoxTag& tag) {
414
5.05k
  static const std::set<BoxTag> kTags = {NewTag("trak"), NewTag("moov"),
415
5.05k
                                         NewTag("mdia"), NewTag("minf")};
416
5.05k
  return kTags.find(tag) != kTags.end();
417
5.05k
}
418
419
// Processes box and returns offset of the next box to process.
420
// A return value of 0 indicates an error.
421
//
422
// Outline of hierarchy and important boxes:
423
// ftyp
424
// moov
425
//   uuid (id is kUuidMoov)
426
//     ... boxes we skip ...
427
//     CMT1 (EXIF data)
428
//     CMT2 (EXIF data)
429
//     ... boxes we skip ...
430
//     THMB (160x120 JPEG thumbnail, embedded in this box)
431
//   trak
432
//     tkhd
433
//     mdia
434
//     ... boxes we skip ...
435
//     minf
436
//       ... boxes we skip ...
437
//       stbl
438
//         stsd
439
//           CRAW (Full image preview, type (JPEG or HEVC), width, height. The
440
//                 image data is found in mdat box, below.)
441
//       ... boxes we skip ...
442
//       stsz (Size of preview, in bytes)
443
//       ... boxes we skip ...
444
//       co64 (Location/offset of full preview data in mdat)
445
//   .. boxes we skip ...
446
// uuid (id is kUuidPrvw)
447
//   PRVW (1620x1080 JPEG preview, embedded in this box)
448
// mdat
449
//   Full image preview (JPEG or HEVC)
450
//   ... RAW image data ...
451
6.41k
size_t ProcessBox(StreamInterface* stream, const Box& box, ProcessData* data) {
452
  // Parse child boxes.
453
6.41k
  if (box.tag() == kUuidTag) {
454
    // Uuid box have extended box types.
455
1.36k
    Uuid uuid;
456
1.36k
    if (stream->GetData(box.header_offset(), uuid.size(), uuid.data()) != kOk) {
457
1
      return 0;
458
1
    }
459
1.36k
    if (uuid == kUuidPrvw) {
460
616
      return box.header_offset() + uuid.size() + 8;
461
744
    } else if (uuid == kUuidMoov) {
462
609
      return box.header_offset() + uuid.size();
463
609
    }  // else skip the box, below.
464
5.05k
  } else if (DoProcessChildren(box.tag())) {
465
27
    return box.header_offset();
466
27
  }
467
468
  // Potentially process the data contained in the box.
469
5.16k
  bool success;
470
5.16k
  if (box.tag() == kMdatTag) {
471
    // mdat_image.offset is relative to mdat's header, update it to be absolute
472
    // offset to the image data.
473
134
    data->mdat_image.offset += box.header_offset();
474
134
    success = true;
475
5.02k
  } else if (box.tag() == kStblTag) {
476
566
    success = ProcessStblBox(stream, box, data);
477
4.46k
  } else if (box.tag() == kPrvwTag) {
478
    // Preview jpeg. 1620x1080 for EOS R.
479
333
    success = ProcessJpegBox(stream, box, &data->prvw_image);
480
4.13k
  } else if (box.tag() == kThmbTag) {
481
    // Thumbnail jpeg. 160x120 for EOS R.
482
95
    success = ProcessJpegBox(stream, box, &data->preview_image_data->thumbnail);
483
4.03k
  } else if (box.tag() == kCmt1Tag) {
484
346
    success =
485
346
        ParseExifIfd0(stream, box.header_offset(), data->preview_image_data);
486
3.68k
  } else if (box.tag() == kCmt2Tag) {
487
872
    success =
488
872
        ParseExifExifIfd(stream, box.header_offset(), data->preview_image_data);
489
2.81k
  } else {
490
    // This box isn't interesting, skip it.
491
2.81k
    success = true;
492
2.81k
  }
493
5.16k
  return success ? box.next_box_offset() : 0;
494
6.41k
}
495
496
bool ProcessStream(StreamInterface* stream, const BoxTag& last_chunk,
497
723
                   ProcessData* data) {
498
723
  size_t offset = 0;
499
6.68k
  while (true) {
500
6.68k
    Box box = GetNextBox(stream, offset);
501
6.68k
    if (!box.IsValid()) {
502
268
      return false;
503
268
    }
504
6.41k
    size_t new_offset = ProcessBox(stream, box, data);
505
6.41k
    if (new_offset <= offset) {
506
321
      return false;
507
321
    }
508
6.09k
    if (box.tag() == last_chunk) {
509
134
      return true;
510
134
    }
511
5.96k
    offset = new_offset;
512
5.96k
  }
513
723
}
514
515
129
bool IsImage(StreamInterface* stream, const Image& image) {
516
129
  if (image.format != Image::kJpegCompressed) {
517
    // Pass responsibility to the caller.
518
5
    return true;
519
5
  }
520
  // Check for JPEG magic number at start. This could be HEVC data.
521
124
  constexpr std::array<uint8_t, 3> kJpegMagicNumber = {0xFF, 0xD8, 0xFF};
522
124
  std::array<uint8_t, 3> magic_number;
523
124
  if (stream->GetData(image.offset, magic_number.size(), magic_number.data()) !=
524
124
      kOk) {
525
15
    return false;
526
15
  }
527
109
  return magic_number == kJpegMagicNumber;
528
124
}
529
530
}  // namespace
531
532
Error Cr3GetPreviewData(StreamInterface* stream,
533
723
                        PreviewImageData* preview_image_data) {
534
723
  ProcessData data{.preview_image_data = preview_image_data};
535
723
  if (!ProcessStream(stream, kMdatTag, &data)) {
536
589
    return kFail;
537
589
  }
538
  // Prefer image in mdata box, as spec ensures it is the largest image.
539
134
  if (data.mdat_image.length > 0 && IsImage(stream, data.mdat_image)) {
540
7
    preview_image_data->preview = data.mdat_image;
541
127
  } else if (data.prvw_image.length > 0 && IsImage(stream, data.prvw_image)) {
542
2
    preview_image_data->preview = data.prvw_image;
543
125
  } else {
544
125
    return kFail;
545
125
  }
546
9
  return kOk;
547
134
}
548
549
0
bool Cr3GetOrientation(StreamInterface* stream, std::uint32_t* orientation) {
550
0
  PreviewImageData preview_image_data;
551
0
  ProcessData data{.preview_image_data = &preview_image_data};
552
0
  if (ProcessStream(stream, kCmt1Tag, &data)) {
553
0
    *orientation = preview_image_data.exif_orientation;
554
0
    return true;
555
0
  }
556
0
  return false;
557
0
}
558
559
}  // namespace piex