Coverage Report

Created: 2026-09-28 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libheif/libheif/image-items/image_item.cc
Line
Count
Source
1
/*
2
 * HEIF image base codec.
3
 * Copyright (c) 2024 Dirk Farin <dirk.farin@gmail.com>
4
 *
5
 * This file is part of libheif.
6
 *
7
 * libheif is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Lesser General Public License as
9
 * published by the Free Software Foundation, either version 3 of
10
 * the License, or (at your option) any later version.
11
 *
12
 * libheif is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public License
18
 * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
#include "image_item.h"
22
#include "mask_image.h"
23
#include "context.h"
24
#include "file.h"
25
#include "jpeg.h"
26
#include "jpeg2000.h"
27
#include "avif.h"
28
#include "avc.h"
29
#include "hevc.h"
30
#include "grid.h"
31
#include "overlay.h"
32
#include "iden.h"
33
#include "tiled.h"
34
#include "codecs/decoder.h"
35
#include "color-conversion/colorconversion.h"
36
#include "api_structs.h"
37
#include "plugin_registry.h"
38
#include "security_limits.h"
39
40
#include <algorithm>
41
#include <limits>
42
#include <cassert>
43
#include <cstring>
44
#include <sstream>
45
//#include <ranges>
46
47
#if WITH_UNCOMPRESSED_CODEC
48
#include "image-items/unc_image.h"
49
#endif
50
51
52
ImageItem::ImageItem(HeifContext* context)
53
20.4k
    : m_heif_context(context)
54
20.4k
{
55
20.4k
  memset(&m_depth_representation_info, 0, sizeof(m_depth_representation_info));
56
20.4k
}
57
58
59
ImageItem::ImageItem(HeifContext* context, heif_item_id id)
60
20.4k
    : ImageItem(context)
61
20.4k
{
62
20.4k
  m_id = id;
63
20.4k
}
64
65
66
bool ImageItem::is_property_essential(const std::shared_ptr<Box>& property) const
67
0
{
68
0
  if (property->get_short_type() == fourcc("ispe")) {
69
0
    return is_ispe_essential();
70
0
  }
71
0
  else {
72
0
    return property->is_essential();
73
0
  }
74
0
}
75
76
77
std::shared_ptr<HeifFile> ImageItem::get_file() const
78
26.8k
{
79
26.8k
  return m_heif_context->get_heif_file();
80
26.8k
}
81
82
83
heif_property_id ImageItem::add_property(const std::shared_ptr<Box>& property, bool essential)
84
3.56k
{
85
3.56k
  if (!property) {
86
85
    return 0;
87
85
  }
88
89
  // HeifFile::add_property() deduplicates the property box, so only remember it here if the
90
  // item does not hold it yet. The returned id is the position in the item's property list and
91
  // is correct in both cases.
92
3.47k
  if (std::find(m_properties.begin(), m_properties.end(), property) == m_properties.end()) {
93
3.47k
    m_properties.push_back(property);
94
3.47k
  }
95
96
3.47k
  return get_file()->add_property(get_id(), property, essential);
97
3.56k
}
98
99
100
heif_property_id ImageItem::add_property_without_deduplication(const std::shared_ptr<Box>& property, bool essential)
101
0
{
102
0
  if (!property) {
103
0
    return 0;
104
0
  }
105
106
0
  m_properties.push_back(property);
107
0
  return get_file()->add_property_without_deduplication(get_id(), property, essential);
108
0
}
109
110
111
heif_compression_format ImageItem::compression_format_from_fourcc_infe_type(uint32_t type)
112
0
{
113
0
  switch (type) {
114
0
    case fourcc("jpeg"):
115
0
      return heif_compression_JPEG;
116
0
    case fourcc("hvc1"):
117
0
      return heif_compression_HEVC;
118
0
    case fourcc("av01"):
119
0
      return heif_compression_AV1;
120
0
    case fourcc("vvc1"):
121
0
      return heif_compression_VVC;
122
0
    case fourcc("j2k1"):
123
0
      return heif_compression_JPEG2000;
124
0
    case fourcc("unci"):
125
0
      return heif_compression_uncompressed;
126
0
    case fourcc("mski"):
127
0
      return heif_compression_mask;
128
0
    default:
129
0
      return heif_compression_undefined;
130
0
  }
131
0
}
132
133
uint32_t ImageItem::compression_format_to_fourcc_infe_type(heif_compression_format format)
134
0
{
135
0
  switch (format) {
136
0
    case heif_compression_JPEG:
137
0
      return fourcc("jpeg");
138
0
    case heif_compression_HEVC:
139
0
      return fourcc("hvc1");
140
0
    case heif_compression_AV1:
141
0
      return fourcc("av01");
142
0
    case heif_compression_VVC:
143
0
      return fourcc("vvc1");
144
0
    case heif_compression_JPEG2000:
145
0
      return fourcc("j2k1");
146
0
    case heif_compression_uncompressed:
147
0
      return fourcc("unci");
148
0
    case heif_compression_mask:
149
0
      return fourcc("mski");
150
0
    default:
151
0
      return 0;
152
0
  }
153
0
}
154
155
156
std::shared_ptr<ImageItem> ImageItem::alloc_for_infe_box(HeifContext* ctx, const std::shared_ptr<Box_infe>& infe)
157
20.4k
{
158
20.4k
  uint32_t item_type = infe->get_item_type_4cc();
159
20.4k
  heif_item_id id = infe->get_item_ID();
160
161
20.4k
  if (item_type == fourcc("jpeg") ||
162
20.1k
      (item_type == fourcc("mime") && infe->get_content_type() == "image/jpeg")) {
163
275
    return std::make_shared<ImageItem_JPEG>(ctx, id);
164
275
  }
165
20.1k
  else if (item_type == fourcc("hvc1")) {
166
12.3k
    return std::make_shared<ImageItem_HEVC>(ctx, id);
167
12.3k
  }
168
7.81k
  else if (item_type == fourcc("av01")) {
169
2.23k
    return std::make_shared<ImageItem_AVIF>(ctx, id);
170
2.23k
  }
171
5.57k
  else if (item_type == fourcc("vvc1")) {
172
15
    return std::make_shared<ImageItem_VVC>(ctx, id);
173
15
  }
174
5.56k
  else if (item_type == fourcc("avc1")) {
175
21
    return std::make_shared<ImageItem_AVC>(ctx, id);
176
21
  }
177
5.54k
  else if (item_type == fourcc("unci")) {
178
#if WITH_UNCOMPRESSED_CODEC
179
    return std::make_shared<ImageItem_uncompressed>(ctx, id);
180
#else
181
    // It is an image item type that we do not support. Thus, generate an ImageItem_Error.
182
183
11
    std::stringstream sstr;
184
11
    sstr << "Image item of type '" << fourcc_to_string(item_type) << "' is not supported.";
185
11
    Error err{ heif_error_Unsupported_feature, heif_suberror_Unsupported_image_type, sstr.str() };
186
11
    return std::make_shared<ImageItem_Error>(ctx, item_type, id, err);
187
11
#endif
188
11
  }
189
5.53k
  else if (item_type == fourcc("j2k1")) {
190
28
    return std::make_shared<ImageItem_JPEG2000>(ctx, id);
191
28
  }
192
5.50k
  else if (item_type == fourcc("lhv1")) {
193
9
    return std::make_shared<ImageItem_Error>(ctx, item_type, id,
194
9
                                             Error{heif_error_Unsupported_feature,
195
9
                                                   heif_suberror_Unsupported_image_type,
196
9
                                                   "Layered HEVC images (lhv1) are not supported yet"});
197
9
  }
198
5.49k
  else if (item_type == fourcc("mski")) {
199
17
    return std::make_shared<ImageItem_mask>(ctx, id);
200
17
  }
201
5.47k
  else if (item_type == fourcc("grid")) {
202
574
    return std::make_shared<ImageItem_Grid>(ctx, id);
203
574
  }
204
4.90k
  else if (item_type == fourcc("iovl")) {
205
84
    return std::make_shared<ImageItem_Overlay>(ctx, id);
206
84
  }
207
4.81k
  else if (item_type == fourcc("iden")) {
208
38
    return std::make_shared<ImageItem_iden>(ctx, id);
209
38
  }
210
#if HEIF_ENABLE_EXPERIMENTAL_FEATURES
211
  else if (item_type == fourcc("tili")) {
212
    return std::make_shared<ImageItem_Tiled>(ctx, id);
213
  }
214
#endif
215
4.78k
  else {
216
    // This item has an unknown type. It could be an image or anything else.
217
    // Do not process the item.
218
219
4.78k
    return nullptr;
220
4.78k
  }
221
20.4k
}
222
223
224
std::shared_ptr<ImageItem> ImageItem::alloc_for_compression_format(HeifContext* ctx, heif_compression_format format)
225
0
{
226
0
  switch (format) {
227
0
    case heif_compression_JPEG:
228
0
      return std::make_shared<ImageItem_JPEG>(ctx);
229
0
    case heif_compression_HEVC:
230
0
      return std::make_shared<ImageItem_HEVC>(ctx);
231
0
    case heif_compression_AV1:
232
0
      return std::make_shared<ImageItem_AVIF>(ctx);
233
0
    case heif_compression_VVC:
234
0
      return std::make_shared<ImageItem_VVC>(ctx);
235
0
    case heif_compression_AVC:
236
0
      return std::make_shared<ImageItem_AVC>(ctx);
237
#if WITH_UNCOMPRESSED_CODEC
238
    case heif_compression_uncompressed:
239
      return std::make_shared<ImageItem_uncompressed>(ctx);
240
#endif
241
0
    case heif_compression_JPEG2000:
242
0
    case heif_compression_HTJ2K:
243
0
      return std::make_shared<ImageItem_JPEG2000>(ctx);
244
0
    case heif_compression_mask:
245
0
      return std::make_shared<ImageItem_mask>(ctx);
246
0
    default:
247
0
      assert(false);
248
0
      return nullptr;
249
0
  }
250
0
}
251
252
253
Result<Encoder::CodedImageData> ImageItem::encode_to_bitstream_and_boxes(const std::shared_ptr<HeifPixelImage>& image,
254
                                                                           heif_encoder* encoder,
255
                                                                           const heif_encoding_options& options,
256
                                                                           heif_image_input_class input_class)
257
0
{
258
  // === generate compressed image bitstream
259
260
0
  Result<Encoder::CodedImageData> encodeResult = encode(image, encoder, options, input_class);
261
0
  if (!encodeResult) {
262
0
    return encodeResult;
263
0
  }
264
265
0
  Encoder::CodedImageData& codedImage = *encodeResult;
266
267
  // === generate properties
268
269
  // --- choose which color profile to put into 'colr' box
270
271
0
  auto colr_boxes = add_color_profile(image, options, input_class, options.output_nclx_profile);
272
0
  codedImage.properties.insert(codedImage.properties.end(),
273
0
                               colr_boxes.begin(),
274
0
                               colr_boxes.end());
275
276
277
  // --- ispe
278
  // Note: 'ispe' must come before the transformation properties
279
280
0
  uint32_t input_width, input_height;
281
0
  input_width = image->get_width();
282
0
  input_height = image->get_height();
283
284
  // --- get the real size of the encoded image
285
286
  // highest priority: codedImageData
287
0
  uint32_t encoded_width = codedImage.encoded_image_width;
288
0
  uint32_t encoded_height = codedImage.encoded_image_height;
289
290
  // second priority: query plugin API
291
0
  if (encoded_width == 0 &&
292
0
      encoder->plugin->plugin_api_version >= 3 &&
293
0
      encoder->plugin->query_encoded_size != nullptr) {
294
295
0
    encoder->plugin->query_encoded_size(encoder->encoder,
296
0
                                        input_width, input_height,
297
0
                                        &encoded_width,
298
0
                                        &encoded_height);
299
0
  }
300
0
  else if (encoded_width == 0) {
301
    // fallback priority: use input size
302
0
    encoded_width = input_width;
303
0
    encoded_height = input_height;
304
0
  }
305
306
0
  auto ispe = std::make_shared<Box_ispe>();
307
0
  ispe->set_size(encoded_width, encoded_height);
308
0
  ispe->set_is_essential(is_ispe_essential());
309
0
  codedImage.properties.push_back(ispe);
310
311
312
  // --- clap (if needed)
313
314
0
  if (input_width != encoded_width ||
315
0
      input_height != encoded_height) {
316
317
0
    auto clap = std::make_shared<Box_clap>();
318
0
    if (Error err = clap->set(input_width, input_height, encoded_width, encoded_height)) {
319
0
      return err;
320
0
    }
321
0
    codedImage.properties.push_back(clap);
322
0
  }
323
324
325
326
  // --- add common metadata properties (pixi, ...)
327
328
0
  auto colorspace = image->get_colorspace();
329
0
  auto chroma = image->get_chroma_format();
330
331
332
  // --- write PIXI property
333
334
0
  std::shared_ptr<Box_pixi> pixi = std::make_shared<Box_pixi>();
335
0
  bool valid_pixi = false;
336
337
0
  if (colorspace == heif_colorspace_filter_array) {
338
    // Skip pixi for filter array images — bit depth info is in uncC
339
0
  }
340
0
  else if (colorspace == heif_colorspace_monochrome) {
341
0
    valid_pixi = pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y));
342
0
  }
343
0
  else if (colorspace == heif_colorspace_YCbCr) {
344
0
    valid_pixi = (pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Y)) &&
345
0
                  pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cb)) &&
346
0
                  pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_Cr)));
347
0
  }
348
0
  else if (colorspace == heif_colorspace_RGB) {
349
0
    if (chroma == heif_chroma_444) {
350
0
      valid_pixi = (pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_R)) &&
351
0
                    pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_G)) &&
352
0
                    pixi->add_channel_bits(image->get_bits_per_pixel(heif_channel_B)));
353
0
    }
354
0
    else if (chroma == heif_chroma_interleaved_RGB ||
355
0
             chroma == heif_chroma_interleaved_RGBA ||
356
0
             chroma == heif_chroma_interleaved_RRGGBB_LE ||
357
0
             chroma == heif_chroma_interleaved_RRGGBB_BE ||
358
0
             chroma == heif_chroma_interleaved_RRGGBBAA_LE ||
359
0
             chroma == heif_chroma_interleaved_RRGGBBAA_BE) {
360
0
      uint16_t bpp = image->get_bits_per_pixel(heif_channel_interleaved);
361
0
      valid_pixi = (pixi->add_channel_bits(bpp) &&
362
0
                    pixi->add_channel_bits(bpp) &&
363
0
                    pixi->add_channel_bits(bpp));
364
0
    }
365
0
  }
366
367
0
  if (valid_pixi) {
368
0
    codedImage.properties.push_back(pixi);
369
0
  }
370
371
  // --- generate properties for image extra data
372
373
  // copy over ImageDescription into image item
374
0
  *static_cast<ImageDescription*>(this) = static_cast<ImageDescription>(*image);
375
376
0
  auto extra_data_properties = image->generate_property_boxes(false);
377
0
  codedImage.properties.insert(codedImage.properties.end(),
378
0
                               extra_data_properties.begin(),
379
0
                               extra_data_properties.end());
380
381
0
  return encodeResult;
382
0
}
383
384
385
Error ImageItem::encode_to_item(HeifContext* ctx,
386
                                const std::shared_ptr<HeifPixelImage>& image,
387
                                heif_encoder* encoder,
388
                                const heif_encoding_options& options,
389
                                heif_image_input_class input_class)
390
0
{
391
0
  uint32_t input_width = image->get_width();
392
0
  uint32_t input_height = image->get_height();
393
394
0
  set_size(input_width, input_height);
395
396
397
  // compress image and assign data to item
398
399
0
  Result<Encoder::CodedImageData> codingResult = encode_to_bitstream_and_boxes(image, encoder, options, input_class);
400
0
  if (!codingResult) {
401
0
    return codingResult.error();
402
0
  }
403
404
0
  Encoder::CodedImageData& codedImage = *codingResult;
405
406
0
  auto infe_result = ctx->get_heif_file()->add_new_infe_box(get_infe_type());
407
0
  if (!infe_result) {
408
0
    return infe_result.error();
409
0
  }
410
0
  auto infe_box = *infe_result;
411
0
  heif_item_id image_id = infe_box->get_item_ID();
412
0
  set_id(image_id);
413
414
0
  ctx->get_heif_file()->append_iloc_data(image_id, codedImage.bitstream, 0);
415
416
417
  // set item properties
418
419
0
  for (auto& propertyBox : codingResult->properties) {
420
0
    bool essential = is_property_essential(propertyBox);
421
422
    // TODO: can we simply use add_property() ?
423
0
    int index = ctx->get_heif_file()->get_ipco_box()->find_or_append_child_box(propertyBox);
424
0
    ctx->get_heif_file()->get_ipma_box()->add_property_for_item_ID(image_id, Box_ipma::PropertyAssociation{essential,
425
0
                                                                                                           uint16_t(index + 1)});
426
0
  }
427
428
429
  // MIAF 7.3.6.7
430
  // This is according to MIAF without Amd2. With Amd2, the restriction has been lifted and the image is MIAF compatible.
431
  // However, since AVIF is based on MIAF, the whole image would be invalid in that case.
432
433
  // We might remove this code at a later point in time when MIAF Amd2 is in wide use.
434
435
0
  if (encoder->plugin->compression_format != heif_compression_AV1 &&
436
0
      image->get_colorspace() == heif_colorspace_YCbCr) {
437
0
    if (!is_integer_multiple_of_chroma_size(image->get_width(),
438
0
                                            image->get_height(),
439
0
                                            image->get_chroma_format())) {
440
0
      mark_not_miaf_compatible();
441
0
    }
442
0
  }
443
444
  // TODO: move this into encode_to_bistream_and_boxes()
445
0
  if (Error err = ctx->get_heif_file()->add_orientation_properties(image_id, options.image_orientation)) {
446
0
    return err;
447
0
  }
448
449
0
  return Error::Ok;
450
0
}
451
452
bool ImageItem::has_ispe_resolution() const
453
0
{
454
0
  return get_property<Box_ispe>() != nullptr;
455
0
}
456
457
uint32_t ImageItem::get_ispe_width() const
458
19.2k
{
459
19.2k
  auto ispe = get_property<Box_ispe>();
460
19.2k
  if (!ispe) {
461
1.03k
    return 0;
462
1.03k
  }
463
18.2k
  else {
464
18.2k
    return ispe->get_width();
465
18.2k
  }
466
19.2k
}
467
468
469
uint32_t ImageItem::get_ispe_height() const
470
19.2k
{
471
19.2k
  auto ispe = get_property<Box_ispe>();
472
19.2k
  if (!ispe) {
473
1.03k
    return 0;
474
1.03k
  }
475
18.2k
  else {
476
18.2k
    return ispe->get_height();
477
18.2k
  }
478
19.2k
}
479
480
481
void ImageItem::get_tile_size(uint32_t& w, uint32_t& h) const
482
0
{
483
0
  w = get_width();
484
0
  h = get_height();
485
0
}
486
487
488
Error ImageItem::postprocess_coded_image_colorspace(heif_colorspace* inout_colorspace, heif_chroma* inout_chroma) const
489
17.1k
{
490
#if 0
491
  auto pixi = m_heif_context->get_heif_file()->get_property<Box_pixi>(id);
492
  if (pixi && pixi->get_num_channels() == 1) {
493
    *out_colorspace = heif_colorspace_monochrome;
494
    *out_chroma = heif_chroma_monochrome;
495
  }
496
#endif
497
498
17.1k
  if (*inout_colorspace == heif_colorspace_YCbCr) {
499
11.5k
    auto nclx = get_color_profile_nclx();
500
11.5k
    if (nclx.get_matrix_coefficients() == 0) {
501
18
      *inout_colorspace = heif_colorspace_RGB;
502
18
      *inout_chroma = heif_chroma_444; // TODO: this or keep the original chroma?
503
18
    }
504
11.5k
  }
505
506
17.1k
  return Error::Ok;
507
17.1k
}
508
509
510
Error ImageItem::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
511
17.6k
{
512
17.6k
  auto decoderResult = get_decoder();
513
17.6k
  if (!decoderResult) {
514
66
    return decoderResult.error();
515
66
  }
516
517
17.5k
  auto decoder = *decoderResult;
518
519
17.5k
  Error err = decoder->get_coded_image_colorspace(out_colorspace, out_chroma);
520
17.5k
  if (err) {
521
401
    return err;
522
401
  }
523
524
17.1k
  postprocess_coded_image_colorspace(out_colorspace, out_chroma);
525
526
17.1k
  return Error::Ok;
527
17.5k
}
528
529
530
int ImageItem::get_luma_bits_per_pixel() const
531
17.4k
{
532
17.4k
  auto decoderResult = get_decoder();
533
17.4k
  if (!decoderResult) {
534
0
    return -1;
535
0
  }
536
537
17.4k
  auto decoder = *decoderResult;
538
539
17.4k
  return decoder->get_luma_bits_per_pixel();
540
17.4k
}
541
542
543
int ImageItem::get_chroma_bits_per_pixel() const
544
10.4k
{
545
10.4k
  auto decoderResult = get_decoder();
546
10.4k
  if (!decoderResult) {
547
0
    return -1;
548
0
  }
549
550
10.4k
  auto decoder = *decoderResult;
551
552
10.4k
  return decoder->get_chroma_bits_per_pixel();
553
10.4k
}
554
555
556
Result<Encoder::CodedImageData> ImageItem::encode(const std::shared_ptr<HeifPixelImage>& image,
557
                                                  heif_encoder* h_encoder,
558
                                                  const heif_encoding_options& options,
559
                                                  heif_image_input_class input_class)
560
0
{
561
0
  auto encoder = get_encoder();
562
0
  return encoder->encode(image, h_encoder, options, input_class);
563
0
}
564
565
566
void ImageItem::set_alpha_channel(std::shared_ptr<ImageItem> img)
567
135
{
568
135
  m_alpha_channel = std::move(img);
569
135
  if (!m_alpha_channel) {
570
0
    return;
571
0
  }
572
573
  // Avoid emitting a duplicate Alpha description if set_alpha_channel was
574
  // called more than once.
575
246
  for (const auto& d : get_component_descriptions()) {
576
246
    if (d.channel == heif_channel_Alpha) {
577
3
      return;
578
3
    }
579
246
  }
580
581
  // Bit depth of the alpha plane comes from the alpha aux item's coded
582
  // image (typically a monochrome HEVC/AVIF channel). Fall back to 8 bpp
583
  // if the decoder cannot tell us.
584
132
  int alpha_bpp = m_alpha_channel->get_luma_bits_per_pixel();
585
132
  if (alpha_bpp <= 0) {
586
27
    alpha_bpp = 8;
587
27
  }
588
589
132
  ComponentDescription desc;
590
132
  desc.component_id = mint_component_id();
591
132
  desc.channel = heif_channel_Alpha;
592
132
  desc.component_type = heif_cmpd_component_type_alpha;
593
132
  desc.datatype = heif_component_datatype_unsigned_integer;
594
132
  desc.bit_depth = static_cast<uint16_t>(alpha_bpp);
595
132
  desc.width = get_ispe_width();
596
132
  desc.height = get_ispe_height();
597
132
  desc.has_data_plane = true;
598
132
  add_component_description(std::move(desc));
599
132
}
600
601
602
void ImageItem::populate_component_descriptions()
603
31.0k
{
604
  // Idempotent: a subclass override (e.g. unci) may already have populated.
605
31.0k
  if (!get_component_descriptions().empty()) {
606
0
    return;
607
0
  }
608
609
  // Visual codecs (HEVC/AVC/AVIF/JPEG/JPEG2000/VVC). Requires the decoder to
610
  // be initialized so we can read colorspace / chroma / bit depths from the
611
  // codec config. If the decoder isn't ready (e.g. on the encoder-output
612
  // path that doesn't call initialize_decoder, or for items whose codec is
613
  // not supported), bail out and leave m_components empty.
614
31.0k
  auto decoderResult = get_decoder();
615
31.0k
  if (!decoderResult || !*decoderResult) {
616
20.3k
    return;
617
20.3k
  }
618
10.7k
  heif_colorspace colorspace = heif_colorspace_undefined;
619
10.7k
  heif_chroma chroma = heif_chroma_undefined;
620
10.7k
  if (Error err = get_coded_image_colorspace(&colorspace, &chroma); err) {
621
283
    return;
622
283
  }
623
624
10.4k
  uint32_t img_w = get_ispe_width();
625
10.4k
  uint32_t img_h = get_ispe_height();
626
10.4k
  int luma_bpp = get_luma_bits_per_pixel();
627
10.4k
  int chroma_bpp = get_chroma_bits_per_pixel();
628
10.4k
  if (luma_bpp <= 0) luma_bpp = 8;
629
10.4k
  if (chroma_bpp <= 0) chroma_bpp = luma_bpp;
630
631
10.4k
  auto emit = [this](heif_channel ch, uint16_t type, int bpp,
632
25.3k
                     uint32_t w, uint32_t h) {
633
25.3k
    ComponentDescription desc;
634
25.3k
    desc.component_id = mint_component_id();
635
25.3k
    desc.channel = ch;
636
25.3k
    desc.component_type = type;
637
25.3k
    desc.datatype = heif_component_datatype_unsigned_integer;
638
25.3k
    desc.bit_depth = static_cast<uint16_t>(bpp);
639
25.3k
    desc.width = w;
640
25.3k
    desc.height = h;
641
25.3k
    desc.has_data_plane = true;
642
25.3k
    add_component_description(std::move(desc));
643
25.3k
  };
644
645
10.4k
  switch (colorspace) {
646
2.97k
    case heif_colorspace_monochrome:
647
2.97k
      emit(heif_channel_Y, heif_cmpd_component_type_monochrome, luma_bpp, img_w, img_h);
648
2.97k
      break;
649
650
7.47k
    case heif_colorspace_YCbCr: {
651
7.47k
      uint32_t cw = channel_width(img_w, chroma, heif_channel_Cb);
652
7.47k
      uint32_t ch_ = channel_height(img_h, chroma, heif_channel_Cb);
653
7.47k
      emit(heif_channel_Y,  heif_cmpd_component_type_Y,  luma_bpp,   img_w, img_h);
654
7.47k
      emit(heif_channel_Cb, heif_cmpd_component_type_Cb, chroma_bpp, cw,    ch_);
655
7.47k
      emit(heif_channel_Cr, heif_cmpd_component_type_Cr, chroma_bpp, cw,    ch_);
656
7.47k
      break;
657
0
    }
658
659
0
    case heif_colorspace_RGB:
660
0
      emit(heif_channel_R, heif_cmpd_component_type_red,   luma_bpp, img_w, img_h);
661
0
      emit(heif_channel_G, heif_cmpd_component_type_green, luma_bpp, img_w, img_h);
662
0
      emit(heif_channel_B, heif_cmpd_component_type_blue,  luma_bpp, img_w, img_h);
663
0
      break;
664
665
0
    default:
666
      // Other colorspaces (filter_array, nonvisual) are unci-only and are
667
      // populated by the unci override.
668
0
      break;
669
10.4k
  }
670
10.4k
}
671
672
673
bool ImageItem::populate_descriptions_from_child(const ImageItem& child,
674
                                                  uint32_t child_w, uint32_t child_h)
675
36
{
676
36
  const auto& child_descs = child.get_component_descriptions();
677
36
  if (child_descs.empty()) {
678
36
    return false;
679
36
  }
680
681
0
  uint32_t img_w = get_ispe_width();
682
0
  uint32_t img_h = get_ispe_height();
683
0
  if (img_w == 0 || img_h == 0 || child_w == 0 || child_h == 0) {
684
0
    return false;
685
0
  }
686
687
0
  for (const auto& src : child_descs) {
688
0
    ComponentDescription d = src;
689
0
    d.component_id = mint_component_id();
690
0
    if (src.has_data_plane) {
691
      // Preserve subsampling ratio: a half-size chroma plane in the child
692
      // becomes half of img_w/h in the wrapper.
693
0
      uint64_t w64 = static_cast<uint64_t>(img_w) * src.width / child_w;
694
0
      uint64_t h64 = static_cast<uint64_t>(img_h) * src.height / child_h;
695
0
      d.width = static_cast<uint32_t>(w64);
696
0
      d.height = static_cast<uint32_t>(h64);
697
0
    }
698
0
    add_component_description(std::move(d));
699
0
  }
700
0
  return true;
701
0
}
702
703
704
std::vector<std::shared_ptr<Box_colr> >
705
ImageItem::add_color_profile(const std::shared_ptr<HeifPixelImage>& image,
706
                             const heif_encoding_options& options,
707
                             heif_image_input_class input_class,
708
                             const heif_color_profile_nclx* target_heif_nclx)
709
0
{
710
0
  std::vector<std::shared_ptr<Box_colr> > colr_boxes;
711
712
0
  if (input_class == heif_image_input_class_normal || input_class == heif_image_input_class_thumbnail) {
713
    // No color profile for non-visual images (e.g. elevation data)
714
0
    if (image->get_colorspace() == heif_colorspace_custom) {
715
0
      return colr_boxes;
716
0
    }
717
718
0
    auto icc_profile = image->get_color_profile_icc();
719
0
    if (icc_profile) {
720
0
      auto colr = std::make_shared<Box_colr>();
721
0
      colr->set_color_profile(icc_profile);
722
0
      colr_boxes.push_back(colr);
723
0
    }
724
725
726
    // save nclx profile
727
728
0
    bool save_nclx_profile = (options.output_nclx_profile != nullptr);
729
730
    // if there is an ICC profile, only save NCLX when we chose to save both profiles
731
0
    if (icc_profile && !(options.version >= 3 &&
732
0
                         options.save_two_colr_boxes_when_ICC_and_nclx_available)) {
733
0
      save_nclx_profile = false;
734
0
    }
735
736
    // we might have turned off nclx completely because macOS/iOS cannot read it
737
0
    if (options.version >= 4 && options.macOS_compatibility_workaround_no_nclx_profile) {
738
0
      save_nclx_profile = false;
739
0
    }
740
741
0
    if (save_nclx_profile) {
742
0
      auto target_nclx_profile = std::make_shared<color_profile_nclx>();
743
0
      target_nclx_profile->set_from_heif_color_profile_nclx(target_heif_nclx);
744
745
0
      auto colr = std::make_shared<Box_colr>();
746
0
      colr->set_color_profile(target_nclx_profile);
747
0
      colr_boxes.push_back(colr);
748
0
    }
749
0
  }
750
751
0
  return colr_boxes;
752
0
}
753
754
755
Error ImageItem::transform_requested_tile_position_to_original_tile_position(uint32_t& tile_x, uint32_t& tile_y) const
756
0
{
757
0
  Result<std::vector<std::shared_ptr<Box>>> propertiesResult = get_properties();
758
0
  if (!propertiesResult) {
759
0
    return propertiesResult.error();
760
0
  }
761
762
  // The caller's (tile_x, tile_y) are in the *displayed* tile grid, so they
763
  // must be validated against the displayed dimensions, not the in-file ones.
764
  // For rotations of 90°/270° the displayed grid has its columns and rows
765
  // swapped relative to the file. Using the file dims (as before) both let
766
  // out-of-range coordinates through and produced unsigned underflows inside
767
  // the inverse-rotation formulas (e.g. `num_rows - 1 - tile_x` with
768
  // `tile_x >= num_rows`).
769
0
  heif_image_tiling tiling = get_heif_image_tiling();
770
0
  if (Error err = process_image_transformations_on_tiling(tiling)) {
771
0
    return err;
772
0
  }
773
774
0
  if (tile_x >= tiling.num_columns || tile_y >= tiling.num_rows) {
775
0
    return {heif_error_Usage_error,
776
0
            heif_suberror_Unspecified,
777
0
            "Tile coordinate out of range for displayed image"};
778
0
  }
779
780
  // Walk the property chain in reverse, undoing each transformation as we go.
781
  // Track the current (intermediate) tile-grid dimensions so each inverse uses
782
  // the right extent and so 90°/270° rotations swap dims for subsequent steps.
783
0
  uint32_t cur_cols = tiling.num_columns;
784
0
  uint32_t cur_rows = tiling.num_rows;
785
786
0
  for (auto propIter = propertiesResult->rbegin(); propIter != propertiesResult->rend(); propIter++) {
787
0
    if (auto irot = std::dynamic_pointer_cast<Box_irot>(*propIter)) {
788
0
      switch (irot->get_rotation_ccw()) {
789
0
        case 90: {
790
0
          uint32_t tx0 = cur_rows - 1 - tile_y;
791
0
          uint32_t ty0 = tile_x;
792
0
          tile_x = tx0;
793
0
          tile_y = ty0;
794
0
          std::swap(cur_cols, cur_rows);
795
0
          break;
796
0
        }
797
0
        case 270: {
798
0
          uint32_t tx0 = tile_y;
799
0
          uint32_t ty0 = cur_cols - 1 - tile_x;
800
0
          tile_x = tx0;
801
0
          tile_y = ty0;
802
0
          std::swap(cur_cols, cur_rows);
803
0
          break;
804
0
        }
805
0
        case 180: {
806
0
          tile_x = cur_cols - 1 - tile_x;
807
0
          tile_y = cur_rows - 1 - tile_y;
808
0
          break;
809
0
        }
810
0
        case 0:
811
0
          break;
812
0
        default:
813
0
          assert(false);
814
0
          break;
815
0
      }
816
0
    }
817
818
0
    if (auto imir = std::dynamic_pointer_cast<Box_imir>(*propIter)) {
819
0
      switch (imir->get_mirror_direction()) {
820
0
        case heif_transform_mirror_direction_horizontal:
821
0
          tile_x = cur_cols - 1 - tile_x;
822
0
          break;
823
0
        case heif_transform_mirror_direction_vertical:
824
0
          tile_y = cur_rows - 1 - tile_y;
825
0
          break;
826
0
        default:
827
0
          assert(false);
828
0
          break;
829
0
      }
830
0
    }
831
0
  }
832
833
0
  return Error::Ok;
834
0
}
835
836
837
void ImageItem::set_clli(const heif_content_light_level& clli)
838
0
{
839
0
  ImageDescription::set_clli(clli);
840
0
  add_property(create_clli_box(), false);
841
0
}
842
843
844
void ImageItem::set_mdcv(const heif_mastering_display_colour_volume& mdcv)
845
0
{
846
0
  ImageDescription::set_mdcv(mdcv);
847
0
  add_property(create_mdcv_box(), false);
848
0
}
849
850
851
void ImageItem::set_amve(const heif_ambient_viewing_environment& amve)
852
0
{
853
0
  ImageDescription::set_amve(amve);
854
0
  add_property(create_amve_box(), false);
855
0
}
856
857
858
void ImageItem::set_nominal_diffuse_white_luminance(uint32_t luminance)
859
0
{
860
0
  ImageDescription::set_nominal_diffuse_white_luminance(luminance);
861
0
  add_property(create_ndwt_box(), false);
862
0
}
863
864
865
void ImageItem::set_pixel_ratio(uint32_t h, uint32_t v)
866
0
{
867
0
  ImageDescription::set_pixel_ratio(h, v);
868
0
  add_property(create_pasp_box(), false);
869
0
}
870
871
872
void ImageItem::set_color_profile_nclx(const nclx_profile& profile)
873
735
{
874
735
  ImageDescription::set_color_profile_nclx(profile);
875
735
  add_property(create_colr_box_nclx(), false);
876
735
}
877
878
879
void ImageItem::set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile)
880
2.82k
{
881
2.82k
  ImageDescription::set_color_profile_icc(profile);
882
2.82k
  add_property(create_colr_box_icc(), false);
883
2.82k
}
884
885
void ImageItem::set_omaf_image_projection(heif_omaf_image_projection projection)
886
0
{
887
0
  ImageDescription::set_omaf_image_projection(projection);
888
0
  add_property(create_prfr_box(), true);
889
0
}
890
891
892
namespace {
893
894
// Detect cycles in the decode reference graph reached from `root`, following the
895
// same edges the decode recursion follows. This uses an explicit heap worklist
896
// rather than recursion on purpose: the graph depth is influenced by the input
897
// (a chain of derived items, and unbounded when the item-count limit is
898
// disabled), so a recursive walk could exhaust the native stack and crash the
899
// process before any decode, on the read path of an untrusted file. The worklist
900
// grows on the heap instead, so depth is bounded only by available memory.
901
//
902
// It is a depth-first walk. `on_path` holds the items on the current
903
// root-to-node path; reaching one that is already on the path is a cycle.
904
// `verified` memoizes items whose subtree is already proven acyclic, so a shared
905
// sub-image reached through several paths is visited once and the walk stays
906
// linear rather than exponential in the number of root-to-item paths
907
// (cf. the decode amplification bound, GHSA-x8xm-cm2c-cfc8).
908
Error check_decode_reference_cycles(const ImageItem* root)
909
6.66k
{
910
6.66k
  std::set<heif_item_id> on_path;    // items on the current DFS path
911
6.66k
  std::set<heif_item_id> verified;   // items whose subtree is proven acyclic
912
913
  // Collect the decode-input children of an item, in the exact order the decode
914
  // recursion follows them: the derived-image ('dimg') inputs (grid tiles,
915
  // overlay inputs, the 'iden' base) first, then the alpha ('auxl') auxiliary.
916
  // The returned shared_ptrs keep the child ImageItems alive for as long as the
917
  // frame that holds them stays on the worklist.
918
7.47k
  auto collect_children = [](const ImageItem* item) {
919
7.47k
    std::vector<std::shared_ptr<const ImageItem>> children;
920
7.47k
    auto file = item->get_file();
921
7.47k
    auto iref = file ? file->get_iref_box() : nullptr;
922
7.47k
    if (iref) {
923
1.56k
      for (heif_item_id child_id : iref->get_references(item->get_id(), fourcc("dimg"))) {
924
1.16k
        if (auto child = item->get_context()->get_image(child_id, true)) {
925
747
          children.push_back(std::move(child));
926
747
        }
927
1.16k
      }
928
1.56k
    }
929
7.47k
    if (const auto& alpha = item->get_alpha_channel()) {
930
85
      children.push_back(alpha);
931
85
    }
932
7.47k
    return children;
933
7.47k
  };
934
935
  // One worklist frame per item currently on the DFS path. `next` is the index
936
  // of the child to descend into next; when it reaches the end, the item's whole
937
  // subtree has been proven acyclic and the item leaves the path.
938
6.66k
  struct Frame {
939
6.66k
    const ImageItem* item;
940
6.66k
    std::vector<std::shared_ptr<const ImageItem>> children;
941
6.66k
    size_t next = 0;
942
6.66k
  };
943
944
6.66k
  std::vector<Frame> stack;
945
6.66k
  on_path.insert(root->get_id());
946
6.66k
  stack.push_back(Frame{root, collect_children(root), 0});
947
948
14.9k
  while (!stack.empty()) {
949
8.29k
    Frame& top = stack.back();
950
951
8.29k
    if (top.next >= top.children.size()) {
952
      // All children proven acyclic: leave the DFS path and memoize the subtree.
953
7.46k
      heif_item_id done_id = top.item->get_id();
954
7.46k
      on_path.erase(done_id);
955
7.46k
      verified.insert(done_id);
956
7.46k
      stack.pop_back();
957
7.46k
      continue;
958
7.46k
    }
959
960
830
    const ImageItem* child = top.children[top.next++].get();
961
    // From here on `top` must not be used: the push_back below may reallocate
962
    // `stack` and invalidate the reference.
963
964
830
    heif_item_id child_id = child->get_id();
965
830
    if (on_path.find(child_id) != on_path.end()) {
966
3
      return {heif_error_Invalid_input,
967
3
              heif_suberror_Item_reference_cycle,
968
3
              "Image reference cycle"};
969
3
    }
970
827
    if (verified.find(child_id) != verified.end()) {
971
16
      continue;  // subtree already proven acyclic; do not descend into it again
972
16
    }
973
974
811
    on_path.insert(child_id);
975
811
    auto grandchildren = collect_children(child);
976
811
    stack.push_back(Frame{child, std::move(grandchildren), 0});
977
811
  }
978
979
6.65k
  return Error::Ok;
980
6.66k
}
981
982
983
// --- MIAF derived-image dependency constraints (ISO/IEC 23000-22, clause 7.3.11)
984
//
985
// MIAF restricts the derivation chain to a fixed order. From base to top it is:
986
//   coded image(s) -> [iden] -> grid -> [iden] -> overlay -> [iden]
987
// (7.3.11.1), plus: an 'iden' shall not be derived directly from another 'iden'
988
// (7.3.11.2), and a grid tile that is an 'iden' must refer directly to a coded
989
// image (7.3.11.4.1). So, ignoring 'iden', a chain may apply overlay above grid
990
// above the coded base, each at most once. We model that with a "structural
991
// rank": coded=0, grid=1, overlay=2. Walking from the top down, each derived
992
// item must have rank <= the rank its position allows, and it lowers the rank
993
// allowed for its own inputs (grid inputs must be coded; overlay inputs may be
994
// grid or below). 'iden' is transparent to the rank but must not sit directly
995
// on another 'iden'. Only the 'dimg' derivation is constrained here; auxiliary
996
// images are checked as their own fresh chains.
997
enum { MIAF_RANK_CODED = 0, MIAF_RANK_GRID = 1, MIAF_RANK_OVERLAY = 2 };
998
999
int miaf_structural_rank(const ImageItem* item, bool& is_iden)
1000
351
{
1001
351
  uint32_t type = item->get_infe_type();
1002
351
  is_iden = (type == fourcc("iden"));
1003
351
  if (type == fourcc("iovl")) { return MIAF_RANK_OVERLAY; }
1004
351
  if (type == fourcc("grid")) { return MIAF_RANK_GRID; }
1005
339
  return MIAF_RANK_CODED;  // coded image, or 'iden' (rank unused when is_iden)
1006
351
}
1007
1008
// `max_rank` is the highest structural rank allowed at this item's position;
1009
// `parent_is_iden` is true when the immediate parent on the derivation path is
1010
// an 'iden'. `verified` memoizes (item, max_rank, parent_is_iden) triples that
1011
// already passed, keeping a shared sub-image from being re-walked per path.
1012
Error check_miaf_derivation_constraints(const ImageItem* item,
1013
                                        int max_rank, bool parent_is_iden,
1014
                                        std::set<uint64_t>& verified)
1015
351
{
1016
351
  heif_item_id id = item->get_id();
1017
1018
351
  bool is_iden = false;
1019
351
  int rank = miaf_structural_rank(item, is_iden);
1020
1021
351
  if (is_iden) {
1022
2
    if (parent_is_iden) {
1023
0
      return {heif_error_Invalid_input, heif_suberror_Unspecified,
1024
0
              "MIAF: an 'iden' image is derived directly from another 'iden' image"};
1025
0
    }
1026
2
  }
1027
349
  else if (rank > max_rank) {
1028
4
    return {heif_error_Invalid_input, heif_suberror_Unspecified,
1029
4
            "MIAF: derived-image dependencies are not in the order allowed by ISO/IEC 23000-22"};
1030
4
  }
1031
1032
347
  uint64_t key = (static_cast<uint64_t>(id) << 4) |
1033
347
                 (static_cast<uint64_t>(max_rank & 0x3) << 2) |
1034
347
                 (parent_is_iden ? 2u : 0u) | (is_iden ? 1u : 0u);
1035
347
  if (!verified.insert(key).second) {
1036
0
    return Error::Ok;  // already verified in this context
1037
0
  }
1038
1039
  // Rank budget passed to this item's own 'dimg' inputs.
1040
347
  int child_max_rank;
1041
347
  bool child_parent_is_iden;
1042
347
  if (is_iden) {
1043
2
    child_max_rank = max_rank;          // transparent: inputs keep this position
1044
2
    child_parent_is_iden = true;
1045
2
  }
1046
345
  else if (rank == MIAF_RANK_OVERLAY) {
1047
0
    child_max_rank = MIAF_RANK_GRID;    // overlay inputs: grid or below
1048
0
    child_parent_is_iden = false;
1049
0
  }
1050
345
  else if (rank == MIAF_RANK_GRID) {
1051
8
    child_max_rank = MIAF_RANK_CODED;   // grid inputs: coded (or iden -> coded)
1052
8
    child_parent_is_iden = false;
1053
8
  }
1054
337
  else {
1055
337
    return Error::Ok;                   // coded image: leaf of the derivation chain
1056
337
  }
1057
1058
10
  auto file = item->get_file();
1059
10
  auto iref = file ? file->get_iref_box() : nullptr;
1060
10
  if (iref) {
1061
17
    for (heif_item_id child_id : iref->get_references(id, fourcc("dimg"))) {
1062
17
      auto child = item->get_context()->get_image(child_id, true);
1063
17
      if (child) {
1064
4
        if (Error err = check_miaf_derivation_constraints(child.get(), child_max_rank,
1065
4
                                                          child_parent_is_iden, verified)) {
1066
4
          return err;
1067
4
        }
1068
4
      }
1069
17
    }
1070
9
  }
1071
1072
  // An auxiliary (e.g. alpha) image is a separate image whose own derivation
1073
  // chain must independently satisfy MIAF, so check it as a fresh chain.
1074
6
  if (auto alpha = item->get_alpha_channel()) {
1075
0
    if (Error err = check_miaf_derivation_constraints(alpha.get(), MIAF_RANK_OVERLAY,
1076
0
                                                      /*parent_is_iden=*/false, verified)) {
1077
0
      return err;
1078
0
    }
1079
0
  }
1080
1081
6
  return Error::Ok;
1082
6
}
1083
1084
} // namespace
1085
1086
1087
Error ImageItem::verify_decodable() const
1088
6.66k
{
1089
  // Always: reject a cyclic decode reference graph (both 'dimg' and 'auxl'
1090
  // edges) before decoding. See the declaration in image_item.h.
1091
6.66k
  if (Error err = check_decode_reference_cycles(this)) {
1092
3
    return err;
1093
3
  }
1094
1095
  // Optionally: enforce MIAF's restricted derived-image dependencies
1096
  // (ISO/IEC 23000-22, clause 7.3.11). Applied when the file declares the 'miaf'
1097
  // brand.
1098
  //
1099
  // TODO(v1.24.x): also apply this when a security-limits flag
1100
  // (always_apply_MIAF_derivation_constraints) is set, so that a malicious file
1101
  // cannot bypass the check simply by omitting the 'miaf' brand. That flag is a
1102
  // heif_security_limits API addition and therefore has to wait for v1.24.x.
1103
6.65k
  bool apply_miaf = false;
1104
6.65k
  if (auto file = get_file()) {
1105
6.65k
    if (auto ftyp = file->get_ftyp_box()) {
1106
6.65k
      apply_miaf = ftyp->has_compatible_brand(heif_brand2_miaf);
1107
6.65k
    }
1108
6.65k
  }
1109
1110
6.65k
  if (apply_miaf) {
1111
347
    std::set<uint64_t> verified;
1112
347
    if (Error err = check_miaf_derivation_constraints(this, MIAF_RANK_OVERLAY,
1113
347
                                                      /*parent_is_iden=*/false, verified)) {
1114
4
      return err;
1115
4
    }
1116
347
  }
1117
1118
6.65k
  return Error::Ok;
1119
6.65k
}
1120
1121
1122
Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decoding_options& options,
1123
                                                                bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
1124
                                                                DecodeTraversalState decode_state) const
1125
6.72k
{
1126
  // Check for cycles before taking m_decode_mutex: a derived item that
1127
  // (transitively) references itself would otherwise re-enter decode_image()
1128
  // on the same ImageItem and self-deadlock on the non-recursive mutex.
1129
  // The matching insert lives inside decode_compressed_image() of derived
1130
  // items (grid/overlay/iden), so the current item is in decode_state only
1131
  // when called from one of its own descendants.
1132
  //
1133
  // Second-layer hardening, not required for correctness: the top-level decode
1134
  // already ran ImageItem::verify_decodable() (HeifContext::decode_image), which
1135
  // proves the whole reachable decode graph is acyclic before any recursion, so
1136
  // this per-path check can never fire on a graph that reached here. It is kept
1137
  // as a cheap in-decode backstop, and the same applies to the equivalent checks
1138
  // in decode_compressed_image() and in grid/overlay/iden. We may remove them in
1139
  // the future once verify_decodable() is the sole cycle guard.
1140
6.72k
  if (decode_state.processed_ids.contains(m_id)) {
1141
0
    return Error{heif_error_Invalid_input,
1142
0
                 heif_suberror_Unspecified,
1143
0
                 "'iref' has cyclic references"};
1144
0
  }
1145
1146
  // Bound the total number of sub-image decodes for this top-level decode.
1147
  // Derived images (grid/iovl/iden) can reference the same base image through
1148
  // indirection, and because the cycle-detection set is per-path, a shared
1149
  // subtree is otherwise re-decoded once per path that reaches it, which grows
1150
  // as branch^depth for nested references. This is the single choke point that
1151
  // every item decode passes through. (GHSA-x8xm-cm2c-cfc8)
1152
6.72k
  if (!decode_state.count_decode()) {
1153
0
    return Error{heif_error_Invalid_input,
1154
0
                 heif_suberror_Security_limit_exceeded,
1155
0
                 "Too many derived-image decode operations (possible reference amplification)"};
1156
0
  }
1157
1158
6.72k
  if (m_item_error) {
1159
0
    return m_item_error;
1160
0
  }
1161
1162
6.72k
  std::lock_guard<std::mutex> lock(m_decode_mutex);
1163
1164
  // --- check whether image size (according to 'ispe') exceeds maximum
1165
1166
6.72k
  if (!decode_tile_only) {
1167
6.72k
    auto ispe = get_property<Box_ispe>();
1168
6.72k
    if (ispe) {
1169
6.69k
      Error err = check_for_valid_image_size(get_context()->get_security_limits(), ispe->get_width(), ispe->get_height());
1170
6.69k
      if (err) {
1171
8
        return err;
1172
8
      }
1173
6.69k
    }
1174
6.72k
  }
1175
1176
1177
  // --- transform tile position
1178
1179
6.72k
  if (decode_tile_only && options.ignore_transformations == false) {
1180
0
    if (Error error = transform_requested_tile_position_to_original_tile_position(tile_x0, tile_y0)) {
1181
0
      return error;
1182
0
    }
1183
0
  }
1184
1185
  // --- decode image
1186
1187
6.72k
  Result<std::shared_ptr<HeifPixelImage>> decodingResult = decode_compressed_image(options, decode_tile_only, tile_x0, tile_y0, decode_state);
1188
6.72k
  if (!decodingResult) {
1189
4.72k
    return decodingResult.error();
1190
4.72k
  }
1191
1192
1.99k
  auto img = *decodingResult;
1193
1.99k
  if (!img) {
1194
    // Safety net: no known decoding path returns a null image without an error anymore.
1195
0
    return Error(heif_error_Decoder_plugin_error, heif_suberror_Unspecified,
1196
0
                 "Decoding returned no image");
1197
0
  }
1198
1199
  // --- validate the decoded image against the signaled size (pre-transform)
1200
1201
1.99k
  if (Error err = check_decoded_image_size(*img, decode_tile_only, tile_x0, tile_y0)) {
1202
684
    return err;
1203
684
  }
1204
1205
1.31k
  std::shared_ptr<HeifFile> file = m_heif_context->get_heif_file();
1206
1207
1208
  // --- apply image transformations
1209
1210
1.31k
  if (options.ignore_transformations == false) {
1211
1.31k
    Result<std::vector<std::shared_ptr<Box>>> propertiesResult = get_properties();
1212
1.31k
    if (!propertiesResult) {
1213
0
      return propertiesResult.error();
1214
0
    }
1215
1216
1.31k
    const std::vector<std::shared_ptr<Box>>& properties = *propertiesResult;
1217
1218
3.97k
    for (const auto& property : properties) {
1219
3.97k
      if (auto rot = std::dynamic_pointer_cast<Box_irot>(property)) {
1220
0
        auto rotateResult = img->rotate_ccw(rot->get_rotation_ccw(), m_heif_context->get_security_limits());
1221
0
        if (!rotateResult) {
1222
0
          return rotateResult.error();
1223
0
        }
1224
1225
0
        img = *rotateResult;
1226
0
      }
1227
1228
1229
3.97k
      if (auto mirror = std::dynamic_pointer_cast<Box_imir>(property)) {
1230
0
        auto mirrorResult = img->mirror_inplace(mirror->get_mirror_direction(),
1231
0
                                                get_context()->get_security_limits());
1232
0
        if (!mirrorResult) {
1233
0
          return mirrorResult.error();
1234
0
        }
1235
0
        img = *mirrorResult;
1236
0
      }
1237
1238
1239
3.97k
      if (!decode_tile_only) {
1240
        // For tiles decoding, we do not process the 'clap' because this is handled by a shift of the tiling grid.
1241
1242
3.97k
        if (auto clap = std::dynamic_pointer_cast<Box_clap>(property)) {
1243
0
          std::shared_ptr<HeifPixelImage> clap_img;
1244
1245
0
          uint32_t img_width = img->get_width();
1246
0
          uint32_t img_height = img->get_height();
1247
1248
0
          auto clapCrop = clap->get_crop(img_width, img_height);
1249
0
          if (!clapCrop) {
1250
0
            return clapCrop.error();
1251
0
          }
1252
1253
0
          int left = clapCrop->left;
1254
0
          int right = clapCrop->right;
1255
0
          int top = clapCrop->top;
1256
0
          int bottom = clapCrop->bottom;
1257
1258
0
          if (left < 0) { left = 0; }
1259
0
          if (top < 0) { top = 0; }
1260
1261
0
          if ((uint32_t) right >= img_width) { right = img_width - 1; }
1262
0
          if ((uint32_t) bottom >= img_height) { bottom = img_height - 1; }
1263
1264
0
          if (left > right ||
1265
0
              top > bottom) {
1266
0
            return Error(heif_error_Invalid_input,
1267
0
                         heif_suberror_Invalid_clean_aperture);
1268
0
          }
1269
1270
0
          auto cropResult = img->crop(left, right, top, bottom, m_heif_context->get_security_limits());
1271
0
          if (!cropResult) {
1272
0
            return cropResult.error();
1273
0
          }
1274
1275
0
          img = *cropResult;
1276
0
        }
1277
3.97k
      }
1278
1279
1280
3.97k
      if (auto iscl = std::dynamic_pointer_cast<Box_iscl>(property)) {
1281
0
        return Error(heif_error_Unsupported_feature,
1282
0
                     heif_suberror_Unspecified,
1283
0
                     "Image scaling (iscl) transformative property is not yet supported");
1284
0
      }
1285
3.97k
    }
1286
1.31k
  }
1287
1288
1289
  // --- add alpha channel, if available
1290
1291
  // TODO: this if statement is probably wrong. When we have a tiled image with alpha
1292
  // channel, then the alpha images should be associated with their respective tiles.
1293
  // However, the tile images are not part of the m_all_images list.
1294
  // Fix this, when we have a test image available.
1295
1296
1.31k
  std::shared_ptr<ImageItem> alpha_image = get_alpha_channel();
1297
1.31k
  if (alpha_image) {
1298
51
    if (alpha_image->get_item_error()) {
1299
2
      return alpha_image->get_item_error();
1300
2
    }
1301
1302
    // Record this item on the current decode path before following the alpha
1303
    // ('auxl') edge. Unlike the derived-image ('dimg') edges, whose cycle-guard
1304
    // insert happens inside decode_compressed_image(), the alpha edge is
1305
    // followed here in the base decode_image() using this frame's own
1306
    // decode_state. decode_compressed_image() only received a *copy* of it, so
1307
    // its insert of m_id is invisible here. Without adding m_id ourselves, a
1308
    // cycle of alpha references (auxl A->B, B->A) would re-enter decode_image()
1309
    // on an item whose non-recursive m_decode_mutex is still held one frame up,
1310
    // deadlocking the decode thread. (GHSA-8fmq-r4pf-7m57)
1311
49
    decode_state.processed_ids.insert(m_id);
1312
1313
49
    auto alphaDecodingResult = alpha_image->decode_image(options, decode_tile_only, tile_x0, tile_y0, decode_state);
1314
49
    if (!alphaDecodingResult) {
1315
10
      return alphaDecodingResult.error();
1316
10
    }
1317
1318
39
    std::shared_ptr<HeifPixelImage> alpha = *alphaDecodingResult;
1319
1320
    // TODO: check that sizes are the same and that we have an Y channel
1321
    // BUT: is there any indication in the standard that the alpha channel should have the same size?
1322
1323
    // TODO: convert in case alpha is decoded as RGB interleaved
1324
1325
39
    heif_channel channel;
1326
39
    switch (alpha->get_colorspace()) {
1327
22
      case heif_colorspace_YCbCr:
1328
39
      case heif_colorspace_monochrome:
1329
39
        channel = heif_channel_Y;
1330
39
        break;
1331
0
      case heif_colorspace_RGB:
1332
0
        channel = heif_channel_R;
1333
0
        break;
1334
0
      case heif_colorspace_undefined:
1335
0
      default:
1336
0
        return Error(heif_error_Invalid_input,
1337
0
                     heif_suberror_Unsupported_color_conversion);
1338
39
    }
1339
1340
1341
    // TODO: we should include a decoding option to control whether libheif should automatically scale the alpha channel, and if so, which scaling filter (enum: Off, NN, Bilinear, ...).
1342
    //       It might also be that a specific output format implies that alpha is scaled (RGBA32). That would favor an enum for the scaling filter option + a bool to switch auto-filtering on.
1343
    //       But we can only do this when libheif itself doesn't assume anymore that the alpha channel has the same resolution.
1344
1345
39
    if ((alpha->get_width() != img->get_width()) || (alpha->get_height() != img->get_height())) {
1346
5
      std::shared_ptr<HeifPixelImage> scaled_alpha;
1347
5
      Error err = alpha->scale_nearest_neighbor(scaled_alpha, img->get_width(), img->get_height(), m_heif_context->get_security_limits());
1348
5
      if (err) {
1349
0
        return err;
1350
0
      }
1351
5
      alpha = std::move(scaled_alpha);
1352
5
    }
1353
39
    if (Error err = img->transfer_channel_from_image_as(alpha, channel, heif_channel_Alpha)) {
1354
0
      return err;
1355
0
    }
1356
1357
39
    if (is_premultiplied_alpha()) {
1358
0
      img->set_premultiplied_alpha(true);
1359
0
    }
1360
39
  }
1361
1362
1363
  // --- set color profile
1364
1365
  // If there is an NCLX profile in the HEIF/AVIF metadata, use this for the color conversion.
1366
  // Otherwise, use the profile that is stored in the image stream itself and then set the
1367
  // (non-NCLX) profile later.
1368
1.30k
  const auto heif_nclx = get_color_profile_nclx();
1369
1.30k
  if (heif_nclx.is_defined()) {
1370
1371
    // Since we have a HEIF colr box, we overwrite the bitstream's CICP parameter
1372
    // with that parameter from the colr box.
1373
94
    nclx_profile consolidated_nclx = heif_nclx;
1374
1375
    // If the decoder plugin populated an NCLX profile from the bitstream's
1376
    // color signalling (e.g. HEVC SPS VUI, AV1 sequence header), compare it
1377
    // against the colr box. Per ISO/IEC 14496-12 and ISO/IEC 23000-22 (MIAF)
1378
    // the colr box overrides the bitstream, but a mismatch is a strong
1379
    // indication of a muxer bug.
1380
94
    const auto bitstream_nclx = img->get_color_profile_nclx();
1381
94
    if (bitstream_nclx.is_defined()) {
1382
1383
      // Check whether there is a CICP mismatch between the HEIF colr box and the compressed bitstream
1384
      // If yes, output a warning.
1385
1386
266
      auto cicp_mismatch = [](uint16_t bs, uint16_t cr) {
1387
266
        return bs != 2 /*unspecified*/ && cr != 2 && bs != cr;
1388
266
      };
1389
1390
92
      if (cicp_mismatch(bitstream_nclx.m_colour_primaries,        heif_nclx.m_colour_primaries)        ||
1391
89
          cicp_mismatch(bitstream_nclx.m_transfer_characteristics, heif_nclx.m_transfer_characteristics) ||
1392
85
          cicp_mismatch(bitstream_nclx.m_matrix_coefficients,     heif_nclx.m_matrix_coefficients)     ||
1393
82
          bitstream_nclx.m_full_range_flag != heif_nclx.m_full_range_flag) {
1394
13
        std::stringstream msg;
1395
13
        msg << "colr box NCLX ("
1396
13
            << heif_nclx.m_colour_primaries << "/"
1397
13
            << heif_nclx.m_transfer_characteristics << "/"
1398
13
            << heif_nclx.m_matrix_coefficients << "/"
1399
13
            << (heif_nclx.m_full_range_flag ? "full" : "limited")
1400
13
            << ") disagrees with bitstream signalling ("
1401
13
            << bitstream_nclx.m_colour_primaries << "/"
1402
13
            << bitstream_nclx.m_transfer_characteristics << "/"
1403
13
            << bitstream_nclx.m_matrix_coefficients << "/"
1404
13
            << (bitstream_nclx.m_full_range_flag ? "full" : "limited")
1405
13
            << "); colr takes precedence per ISO/IEC 14496-12 and ISO/IEC 23000-22 (MIAF)";
1406
13
        add_decoding_warning({heif_error_Invalid_input,
1407
13
                              heif_suberror_NCLX_colr_VUI_mismatch,
1408
13
                              msg.str()});
1409
13
      }
1410
1411
      // Fix full-range flag in images that are probably broken.
1412
1413
92
      if (options.version >= 9 && options.autocorrect_broken_input) {
1414
1415
        // Some Sony cameras mis-tag full_range_flag=0 in colr while the bitstream VUI is correct (full_range_flag=1), see issue #1770.
1416
        // Rationale for the fix: if the bitstream explicitly says full_range=1, it probably does with for a reason.
1417
        // Thus, we keep the full-range flag.
1418
1419
0
        if (bitstream_nclx.get_full_range_flag() == true &&
1420
0
            heif_nclx.get_full_range_flag() == false) {
1421
0
          add_decoding_warning({
1422
0
                                 heif_error_Invalid_input,
1423
0
                                 heif_suberror_NCLX_colr_VUI_mismatch,
1424
0
                                 "Autocorrecting full-range flag to ON (colr=limited, bitstream=full)"
1425
0
                               });
1426
1427
0
          consolidated_nclx.set_full_range_flag(true);
1428
0
        }
1429
0
      }
1430
92
    }
1431
1432
94
    img->set_color_profile_nclx(consolidated_nclx);
1433
94
  }
1434
1435
1.30k
  auto icc = get_color_profile_icc();
1436
1.30k
  if (icc) {
1437
147
    img->set_color_profile_icc(icc);
1438
147
  }
1439
1440
1441
  // --- attach metadata to image
1442
1443
1.30k
  {
1444
1.30k
    auto ipco_box = file->get_ipco_box();
1445
1.30k
    auto ipma_box = file->get_ipma_box();
1446
1447
    // CLLI
1448
1449
1.30k
    auto clli = get_property<Box_clli>();
1450
1.30k
    if (clli) {
1451
0
      img->set_clli(clli->clli);
1452
0
    }
1453
1454
    // MDCV
1455
1456
1.30k
    auto mdcv = get_property<Box_mdcv>();
1457
1.30k
    if (mdcv) {
1458
0
      img->set_mdcv(mdcv->mdcv);
1459
0
    }
1460
1461
    // AMVE
1462
1463
1.30k
    auto amve = get_property<Box_amve>();
1464
1.30k
    if (amve) {
1465
0
      img->set_amve(amve->amve);
1466
0
    }
1467
1468
    // NDWT
1469
1470
1.30k
    auto ndwt = get_property<Box_ndwt>();
1471
1.30k
    if (ndwt) {
1472
0
      img->set_nominal_diffuse_white_luminance(ndwt->get_diffuse_white_luminance());
1473
0
    }
1474
1475
    // PASP
1476
1477
1.30k
    auto pasp = get_property<Box_pasp>();
1478
1.30k
    if (pasp) {
1479
0
      img->set_pixel_ratio(pasp->hSpacing, pasp->vSpacing);
1480
0
    }
1481
1482
    // TAI
1483
1484
1.30k
    auto itai = get_property<Box_itai>();
1485
1.30k
    if (itai) {
1486
0
      img->set_tai_timestamp(itai->get_tai_timestamp_packet());
1487
0
    }
1488
1489
    // GIMI content ID
1490
1491
1.30k
    auto gimi_content_id = get_property<Box_gimi_content_id>();
1492
1.30k
    if (gimi_content_id) {
1493
0
      img->set_gimi_sample_content_id(gimi_content_id->get_content_id());
1494
0
    }
1495
1496
    // Image projection (OMAF)
1497
1.30k
    auto prfr = get_property<Box_prfr>();
1498
1.30k
    if (prfr) {
1499
12
      img->set_omaf_image_projection(prfr->get_omaf_image_projection());
1500
12
    }
1501
1.30k
  }
1502
1503
1504
1.30k
  return img;
1505
1.31k
}
1506
1507
#if 0
1508
Result<std::vector<uint8_t>> ImageItem::read_bitstream_configuration_data_override(heif_item_id itemId, heif_compression_format format) const
1509
{
1510
  auto item_codec = ImageItem::alloc_for_compression_format(const_cast<HeifContext*>(get_context()), format);
1511
  assert(item_codec);
1512
1513
  Error err = item_codec->init_decoder_from_item(itemId);
1514
  if (err) {
1515
    return err;
1516
  }
1517
1518
  return item_codec->read_bitstream_configuration_data(itemId);
1519
}
1520
#endif
1521
1522
Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_compressed_image(const heif_decoding_options& options,
1523
                                                                           bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
1524
                                                                           DecodeTraversalState decode_state) const
1525
6.59k
{
1526
6.59k
  if (decode_state.processed_ids.contains(m_id)) {
1527
0
    return Error{heif_error_Invalid_input,
1528
0
                 heif_suberror_Unspecified,
1529
0
                 "'iref' has cyclic references"};
1530
0
  }
1531
1532
6.59k
  decode_state.processed_ids.insert(m_id);
1533
1534
1535
6.59k
  DataExtent extent;
1536
6.59k
  extent.set_from_image_item(get_file(), get_id());
1537
1538
6.59k
  auto decoderResult = get_decoder();
1539
6.59k
  if (!decoderResult) {
1540
0
    return decoderResult.error();
1541
0
  }
1542
1543
6.59k
  auto decoder = *decoderResult;
1544
1545
6.59k
  decoder->set_data_extent(std::move(extent));
1546
1547
  // Tighten max_image_size_pixels for this decode so a decoder plugin (e.g.
1548
  // dav1d) cannot allocate buffers far larger than the ispe-declared size
1549
  // when the codec bitstream lies about its dimensions.
1550
6.59k
  heif_security_limits tightened = tighten_image_size_limit_for_ispe(
1551
6.59k
      get_context()->get_security_limits(),
1552
6.59k
      get_ispe_width(), get_ispe_height(),
1553
6.59k
      max_coding_unit_size_for_codec(get_compression_format()));
1554
1555
6.59k
  return decoder->decode_single_frame_from_compressed_data(options, &tightened);
1556
6.59k
}
1557
1558
1559
Error ImageItem::check_decoded_image_size(const HeifPixelImage& img,
1560
                                          bool decode_tile_only,
1561
                                          uint32_t tile_x0, uint32_t tile_y0) const
1562
1.99k
{
1563
1.99k
  uint32_t expected_w, expected_h;
1564
1565
1.99k
  if (decode_tile_only) {
1566
    // The decoded buffer is a single tile, sized to the signaled tile size.
1567
0
    get_tile_size(expected_w, expected_h);
1568
0
  }
1569
1.99k
  else {
1570
    // Pre-transform coded size from the 'ispe' property.
1571
1.99k
    expected_w = get_ispe_width();
1572
1.99k
    expected_h = get_ispe_height();
1573
1.99k
  }
1574
1575
  // No 'ispe' / no tile size known -> cannot validate (a missing-'ispe' warning is
1576
  // already emitted upstream). Skip rather than reject.
1577
1.99k
  if (expected_w == 0 || expected_h == 0) {
1578
25
    return Error::Ok;
1579
25
  }
1580
1581
1.97k
  if (!img.primary_planes_have_size(expected_w, expected_h)) {
1582
684
    return Error{heif_error_Invalid_input,
1583
684
                 heif_suberror_Invalid_image_size,
1584
684
                 "Decoded image does not have the size signaled in the file."};
1585
684
  }
1586
1587
1.28k
  return Error::Ok;
1588
1.97k
}
1589
1590
1591
heif_image_tiling ImageItem::get_heif_image_tiling() const
1592
0
{
1593
  // --- Return a dummy tiling consisting of only a single tile for the whole image
1594
1595
0
  heif_image_tiling tiling{};
1596
1597
0
  tiling.version = 1;
1598
0
  tiling.num_columns = 1;
1599
0
  tiling.num_rows = 1;
1600
1601
  // Report the coded (pre-transformation) dimensions here. The caller applies
1602
  // the transformative properties (irot, imir, clap) via
1603
  // process_image_transformations_on_tiling(), so handing it the already
1604
  // transformed m_width/m_height would apply them a second time. For a clap
1605
  // that shrinks the image to zero this double application underflowed inside
1606
  // Box_clap::get_crop() (GHSA-jc8f-p23p-5hjg); for irot/imir it silently
1607
  // produced wrong dimensions. The grid/unc/tiled overrides likewise report
1608
  // coded dimensions.
1609
0
  uint32_t coded_width = m_width;
1610
0
  uint32_t coded_height = m_height;
1611
0
  if (has_ispe_resolution()) {
1612
0
    coded_width = get_ispe_width();
1613
0
    coded_height = get_ispe_height();
1614
0
  }
1615
1616
0
  tiling.tile_width = coded_width;
1617
0
  tiling.tile_height = coded_height;
1618
0
  tiling.image_width = coded_width;
1619
0
  tiling.image_height = coded_height;
1620
1621
0
  tiling.top_offset = 0;
1622
0
  tiling.left_offset = 0;
1623
0
  tiling.number_of_extra_dimensions = 0;
1624
1625
0
  for (uint32_t& s : tiling.extra_dimension_size) {
1626
0
    s = 0;
1627
0
  }
1628
1629
0
  return tiling;
1630
0
}
1631
1632
1633
Result<std::vector<std::shared_ptr<Box>>> ImageItem::get_properties() const
1634
1.31k
{
1635
1.31k
  std::vector<std::shared_ptr<Box>> properties;
1636
1.31k
  auto ipco_box = get_file()->get_ipco_box();
1637
1.31k
  auto ipma_box = get_file()->get_ipma_box();
1638
1.31k
  Error error = ipco_box->get_properties_for_item_ID(m_id, ipma_box, properties);
1639
1.31k
  if (error) {
1640
0
    return error;
1641
0
  }
1642
1643
1.31k
  return properties;
1644
1.31k
}
1645
1646
1647
bool ImageItem::has_essential_property_other_than(const std::set<uint32_t>& props) const
1648
0
{
1649
0
  Result<std::vector<std::shared_ptr<Box>>> propertiesResult = get_properties();
1650
0
  if (!propertiesResult) {
1651
0
    return false;
1652
0
  }
1653
1654
0
  for (const auto& property : *propertiesResult) {
1655
0
    if (is_property_essential(property) &&
1656
0
        props.find(property->get_short_type()) == props.end()) {
1657
0
      return true;
1658
0
    }
1659
0
  }
1660
1661
0
  return false;
1662
0
}
1663
1664
1665
Error ImageItem::process_image_transformations_on_tiling(heif_image_tiling& tiling) const
1666
0
{
1667
0
  Result<std::vector<std::shared_ptr<Box>>> propertiesResult = get_properties();
1668
0
  if (!propertiesResult) {
1669
0
    return propertiesResult.error();
1670
0
  }
1671
1672
0
  const std::vector<std::shared_ptr<Box>>& properties = *propertiesResult;
1673
1674
0
  uint32_t left_excess = 0;
1675
0
  uint32_t top_excess = 0;
1676
0
  uint32_t right_excess;
1677
0
  uint32_t bottom_excess;
1678
1679
  // Prevent divide by zero.
1680
1681
0
  if (tiling.tile_width != 0 && tiling.tile_height != 0) {
1682
0
    right_excess = tiling.image_width % tiling.tile_width;
1683
0
    bottom_excess = tiling.image_height % tiling.tile_height;
1684
0
  }
1685
0
  else {
1686
0
    right_excess = 0;
1687
0
    bottom_excess = 0;
1688
0
  }
1689
1690
1691
0
  for (const auto& property : properties) {
1692
1693
    // --- rotation
1694
1695
0
    if (auto rot = std::dynamic_pointer_cast<Box_irot>(property)) {
1696
0
      int angle = rot->get_rotation_ccw();
1697
0
      if (angle == 90 || angle == 270) {
1698
0
        std::swap(tiling.tile_width, tiling.tile_height);
1699
0
        std::swap(tiling.image_width, tiling.image_height);
1700
0
        std::swap(tiling.num_rows, tiling.num_columns);
1701
0
      }
1702
1703
0
      switch (angle) {
1704
0
        case 0:
1705
0
          break;
1706
0
        case 180:
1707
0
          std::swap(left_excess, right_excess);
1708
0
          std::swap(top_excess, bottom_excess);
1709
0
          break;
1710
0
        case 90: {
1711
0
          uint32_t old_top_excess = top_excess;
1712
0
          top_excess = right_excess;
1713
0
          right_excess = bottom_excess;
1714
0
          bottom_excess = left_excess;
1715
0
          left_excess = old_top_excess;
1716
0
          break;
1717
0
        }
1718
0
        case 270: {
1719
0
          uint32_t old_top_excess = top_excess;
1720
0
          top_excess = left_excess;
1721
0
          left_excess = bottom_excess;
1722
0
          bottom_excess = right_excess;
1723
0
          right_excess = old_top_excess;
1724
0
          break;
1725
0
        }
1726
0
        default:
1727
0
          assert(false);
1728
0
          break;
1729
0
      }
1730
0
    }
1731
1732
    // --- mirror
1733
1734
0
    if (auto mirror = std::dynamic_pointer_cast<Box_imir>(property)) {
1735
0
      switch (mirror->get_mirror_direction()) {
1736
0
        case heif_transform_mirror_direction_horizontal:
1737
0
          std::swap(left_excess, right_excess);
1738
0
          break;
1739
0
        case heif_transform_mirror_direction_vertical:
1740
0
          std::swap(top_excess, bottom_excess);
1741
0
          break;
1742
0
        default:
1743
0
          assert(false);
1744
0
          break;
1745
0
      }
1746
0
    }
1747
1748
    // --- crop
1749
1750
0
    if (auto clap = std::dynamic_pointer_cast<Box_clap>(property)) {
1751
0
      std::shared_ptr<HeifPixelImage> clap_img;
1752
1753
0
      auto cropResult = clap->get_crop(tiling.image_width, tiling.image_height);
1754
0
      if (!cropResult) {
1755
0
        return cropResult.error();
1756
0
      }
1757
1758
0
      int left = cropResult->left;
1759
0
      int right = cropResult->right;
1760
0
      int top = cropResult->top;
1761
0
      int bottom = cropResult->bottom;
1762
1763
0
      if (left < 0) { left = 0; }
1764
0
      if (top < 0) { top = 0; }
1765
1766
0
      if ((uint32_t)right >= tiling.image_width) { right = tiling.image_width - 1; }
1767
0
      if ((uint32_t)bottom >= tiling.image_height) { bottom = tiling.image_height - 1; }
1768
1769
0
      if (left > right ||
1770
0
          top > bottom) {
1771
0
        return {heif_error_Invalid_input,
1772
0
                heif_suberror_Invalid_clean_aperture};
1773
0
      }
1774
1775
0
      left_excess += left;
1776
0
      right_excess += right;
1777
0
      top_excess += top;
1778
0
      bottom_excess += bottom;
1779
0
    }
1780
1781
    // --- scaling (not supported yet)
1782
1783
0
    if (auto iscl = std::dynamic_pointer_cast<Box_iscl>(property)) {
1784
0
      return {heif_error_Unsupported_feature,
1785
0
              heif_suberror_Unspecified,
1786
0
              "Image scaling (iscl) transformative property is not yet supported"};
1787
0
    }
1788
0
  }
1789
1790
0
  tiling.left_offset = left_excess;
1791
0
  tiling.top_offset = top_excess;
1792
1793
0
  return Error::Ok;
1794
0
}