Coverage Report

Created: 2026-09-14 07:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libheif/libheif/image-items/overlay.cc
Line
Count
Source
1
/*
2
 * HEIF 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 "overlay.h"
22
#include "context.h"
23
#include "file.h"
24
#include "color-conversion/colorconversion.h"
25
#include "security_limits.h"
26
27
#include <utility>
28
29
30
template<typename I>
31
void writevec(uint8_t* data, size_t& idx, I value, int len)
32
0
{
33
0
  for (int i = 0; i < len; i++) {
34
0
    data[idx + i] = static_cast<uint8_t>((value >> (len - 1 - i) * 8) & 0xFF);
35
0
  }
36
37
0
  idx += len;
38
0
}
Unexecuted instantiation: void writevec<unsigned short>(unsigned char*, unsigned long&, unsigned short, int)
Unexecuted instantiation: void writevec<unsigned int>(unsigned char*, unsigned long&, unsigned int, int)
Unexecuted instantiation: void writevec<int>(unsigned char*, unsigned long&, int, int)
39
40
41
static int32_t readvec_signed(const std::vector<uint8_t>& data, int& ptr, int len)
42
216
{
43
216
  const uint32_t high_bit = UINT32_C(0x80) << ((len - 1) * 8);
44
45
216
  uint32_t val = 0;
46
648
  while (len--) {
47
432
    val <<= 8;
48
432
    val |= data[ptr++];
49
432
  }
50
51
216
  bool negative = (val & high_bit) != 0;
52
53
216
  if (negative) {
54
112
    return -static_cast<int32_t>((~val) & 0x7fffffff) -1;
55
112
  }
56
104
  else {
57
104
    return static_cast<int32_t>(val);
58
104
  }
59
60
0
  return val;
61
216
}
62
63
64
static uint32_t readvec(const std::vector<uint8_t>& data, int& ptr, int len)
65
378
{
66
378
  uint32_t val = 0;
67
1.13k
  while (len--) {
68
756
    val <<= 8;
69
756
    val |= data[ptr++];
70
756
  }
71
72
378
  return val;
73
378
}
74
75
76
Error ImageOverlay::parse(size_t num_images, const std::vector<uint8_t>& data)
77
83
{
78
83
  Error eofError(heif_error_Invalid_input,
79
83
                 heif_suberror_Invalid_overlay_data,
80
83
                 "Overlay image data incomplete");
81
82
83
  if (data.size() < 2 + 4 * 2) {
83
7
    return eofError;
84
7
  }
85
86
76
  m_version = data[0];
87
76
  if (m_version != 0) {
88
7
    std::stringstream sstr;
89
7
    sstr << "Overlay image data version " << ((int) m_version) << " is not implemented yet";
90
91
7
    return {heif_error_Unsupported_feature,
92
7
            heif_suberror_Unsupported_data_version,
93
7
            sstr.str()};
94
7
  }
95
96
69
  m_flags = data[1];
97
98
69
  int field_len = ((m_flags & 1) ? 4 : 2);
99
69
  int ptr = 2;
100
101
69
  if (ptr + 4 * 2 + 2 * field_len + num_images * 2 * field_len > data.size()) {
102
6
    return eofError;
103
6
  }
104
105
315
  for (int i = 0; i < 4; i++) {
106
252
    uint16_t color = static_cast<uint16_t>(readvec(data, ptr, 2));
107
252
    m_background_color[i] = color;
108
252
  }
109
110
63
  m_width = readvec(data, ptr, field_len);
111
63
  m_height = readvec(data, ptr, field_len);
112
113
63
  if (m_width == 0 || m_height == 0) {
114
9
    return {heif_error_Invalid_input,
115
9
            heif_suberror_Invalid_overlay_data,
116
9
            "Overlay image with zero width or height."};
117
9
  }
118
119
54
  m_offsets.resize(num_images);
120
121
162
  for (size_t i = 0; i < num_images; i++) {
122
108
    m_offsets[i].x = readvec_signed(data, ptr, field_len);
123
108
    m_offsets[i].y = readvec_signed(data, ptr, field_len);
124
108
  }
125
126
54
  return Error::Ok;
127
63
}
128
129
130
std::vector<uint8_t> ImageOverlay::write() const
131
0
{
132
0
  assert(m_version == 0);
133
134
0
  bool longFields = (m_width > 0xFFFF) || (m_height > 0xFFFF);
135
0
  for (const auto& img : m_offsets) {
136
0
    if (img.x > 0x7FFF || img.y > 0x7FFF || img.x < -32768 || img.y < -32768) {
137
0
      longFields = true;
138
0
      break;
139
0
    }
140
0
  }
141
142
0
  std::vector<uint8_t> data;
143
144
0
  data.resize(2 + 4 * 2 + (longFields ? 4 : 2) * (2 + m_offsets.size() * 2));
145
146
0
  size_t idx = 0;
147
0
  data[idx++] = m_version;
148
0
  data[idx++] = (longFields ? 1 : 0); // flags
149
150
0
  for (uint16_t color : m_background_color) {
151
0
    writevec(data.data(), idx, color, 2);
152
0
  }
153
154
0
  writevec(data.data(), idx, m_width, longFields ? 4 : 2);
155
0
  writevec(data.data(), idx, m_height, longFields ? 4 : 2);
156
157
0
  for (const auto& img : m_offsets) {
158
0
    writevec(data.data(), idx, img.x, longFields ? 4 : 2);
159
0
    writevec(data.data(), idx, img.y, longFields ? 4 : 2);
160
0
  }
161
162
0
  assert(idx == data.size());
163
164
0
  return data;
165
0
}
166
167
168
std::string ImageOverlay::dump() const
169
0
{
170
0
  std::stringstream sstr;
171
172
0
  sstr << "version: " << ((int) m_version) << "\n"
173
0
       << "flags: " << ((int) m_flags) << "\n"
174
0
       << "background color: " << m_background_color[0]
175
0
       << ";" << m_background_color[1]
176
0
       << ";" << m_background_color[2]
177
0
       << ";" << m_background_color[3] << "\n"
178
0
       << "canvas size: " << m_width << "x" << m_height << "\n"
179
0
       << "offsets: ";
180
181
0
  for (const ImageWithOffset& offset : m_offsets) {
182
0
    sstr << offset.x << ";" << offset.y << " ";
183
0
  }
184
0
  sstr << "\n";
185
186
0
  return sstr.str();
187
0
}
188
189
190
void ImageOverlay::get_background_color(uint16_t col[4]) const
191
32
{
192
160
  for (int i = 0; i < 4; i++) {
193
128
    col[i] = m_background_color[i];
194
128
  }
195
32
}
196
197
198
void ImageOverlay::get_offset(size_t image_index, int32_t* x, int32_t* y) const
199
0
{
200
0
  assert(image_index < m_offsets.size());
201
0
  assert(x && y);
202
203
0
  *x = m_offsets[image_index].x;
204
0
  *y = m_offsets[image_index].y;
205
0
}
206
207
208
209
ImageItem_Overlay::ImageItem_Overlay(HeifContext* ctx)
210
0
    : ImageItem(ctx)
211
0
{
212
0
}
213
214
215
ImageItem_Overlay::ImageItem_Overlay(HeifContext* ctx, heif_item_id id)
216
198
    : ImageItem(ctx, id)
217
198
{
218
198
}
219
220
221
Error ImageItem_Overlay::initialize_decoder()
222
185
{
223
185
  Error err = read_overlay_spec();
224
185
  if (err) {
225
131
    return err;
226
131
  }
227
228
54
  return Error::Ok;
229
185
}
230
231
232
Error ImageItem_Overlay::read_overlay_spec()
233
185
{
234
185
  auto heif_file = get_context()->get_heif_file();
235
236
185
  auto iref_box = heif_file->get_iref_box();
237
238
185
  if (!iref_box) {
239
13
    return {heif_error_Invalid_input,
240
13
            heif_suberror_No_iref_box,
241
13
            "No iref box available, but needed for iovl image"};
242
13
  }
243
244
245
172
  m_overlay_image_ids = iref_box->get_references(get_id(), fourcc("dimg"));
246
247
  // An overlay with no input images is degenerate: ISO/IEC 23008-12 image-overlay
248
  // derivation places "one or more" input images onto the canvas.
249
172
  if (m_overlay_image_ids.empty()) {
250
60
    return Error(heif_error_Invalid_input,
251
60
                 heif_suberror_Missing_grid_images,
252
60
                 "'iovl' image has no referenced input images");
253
60
  }
254
255
  // Limit the number of images composited into a single overlay. This is a
256
  // hardcoded stopgap (see MAX_OVERLAY_IMAGES) until a configurable limit can be
257
  // added to heif_security_limits in the next major release. Skipped when the
258
  // security limits are disabled. (GHSA-x8xm-cm2c-cfc8)
259
112
  if (get_context()->get_security_limits()->max_items != 0 &&
260
112
      m_overlay_image_ids.size() > MAX_OVERLAY_IMAGES) {
261
6
    return Error(heif_error_Invalid_input,
262
6
                 heif_suberror_Security_limit_exceeded,
263
6
                 "'iovl' image composites more input images than allowed");
264
6
  }
265
266
267
106
  auto overlayDataResult = heif_file->get_uncompressed_item_data(get_id());
268
106
  if (!overlayDataResult) {
269
23
    return overlayDataResult.error();
270
23
  }
271
272
83
  Error err = m_overlay_spec.parse(m_overlay_image_ids.size(), *overlayDataResult);
273
83
  if (err) {
274
29
    return err;
275
29
  }
276
277
54
  if (m_overlay_image_ids.size() != m_overlay_spec.get_num_offsets()) {
278
0
    return Error(heif_error_Invalid_input,
279
0
                 heif_suberror_Invalid_overlay_data,
280
0
                 "Number of image offsets does not match the number of image references");
281
0
  }
282
283
54
  return Error::Ok;
284
54
}
285
286
287
Result<std::shared_ptr<HeifPixelImage>> ImageItem_Overlay::decode_compressed_image(const heif_decoding_options& options,
288
                                                                                   bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
289
                                                                                   DecodeTraversalState decode_state) const
290
39
{
291
39
  return decode_overlay_image(options, decode_state);
292
39
}
293
294
// Note: ImageItem_Overlay does not override check_decoded_image_size(). The overlay
295
// canvas is built to the overlay-header size by construction (decode_overlay_image
296
// creates it at m_overlay_spec canvas size), so checking it against that same size
297
// would be tautological. The base default checks the canvas against 'ispe', which is
298
// the meaningful cross-check (overlay-header size vs signaled size).
299
300
301
Result<std::shared_ptr<HeifPixelImage>> ImageItem_Overlay::decode_overlay_image(const heif_decoding_options& options,
302
                                                                                DecodeTraversalState decode_state) const
303
39
{
304
39
  if (decode_state.processed_ids.contains(get_id())) {
305
0
    return Error{heif_error_Invalid_input,
306
0
                 heif_suberror_Unspecified,
307
0
                 "'iref' has cyclic references"};
308
0
  }
309
310
39
  decode_state.processed_ids.insert(get_id());
311
312
  // Bound the depth of overlays nested inside one another. Real files place at
313
  // most one overlay in a decode chain; deep nesting is a fast-rejection path
314
  // for the reference-amplification gadget. decode_state (and thus this counter)
315
  // is copied by value at each hop, so overlay_nesting measures depth along the
316
  // current path only. (GHSA-x8xm-cm2c-cfc8)
317
39
  decode_state.overlay_nesting++;
318
39
  if (decode_state.max_overlay_nesting != 0 &&
319
39
      decode_state.overlay_nesting > decode_state.max_overlay_nesting) {
320
0
    return Error{heif_error_Invalid_input,
321
0
                 heif_suberror_Security_limit_exceeded,
322
0
                 "'iovl' overlay images nested too deeply"};
323
0
  }
324
325
326
39
  std::shared_ptr<HeifPixelImage> img;
327
328
39
  uint32_t w = m_overlay_spec.get_canvas_width();
329
39
  uint32_t h = m_overlay_spec.get_canvas_height();
330
331
39
  Error err = check_for_valid_image_size(get_context()->get_security_limits(), w, h);
332
39
  if (err) {
333
7
    return err;
334
7
  }
335
336
  // TODO: seems we always have to compose this in RGB since the background color is an RGB value
337
32
  img = std::make_shared<HeifPixelImage>();
338
32
  img->create(w, h,
339
32
              heif_colorspace_RGB,
340
32
              heif_chroma_444);
341
32
  if (auto error = img->add_channel(heif_channel_R, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths
342
0
    return error;
343
0
  }
344
32
  if (auto error = img->add_channel(heif_channel_G, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths
345
0
    return error;
346
0
  }
347
32
  if (auto error = img->add_channel(heif_channel_B, w, h, 8, get_context()->get_security_limits())) { // TODO: other bit depths
348
0
    return error;
349
0
  }
350
351
32
  uint16_t bkg_color[4];
352
32
  m_overlay_spec.get_background_color(bkg_color);
353
354
32
  err = img->fill_RGB_16bit(bkg_color[0], bkg_color[1], bkg_color[2], bkg_color[3]);
355
32
  if (err) {
356
0
    return err;
357
0
  }
358
359
32
  for (size_t i = 0; i < m_overlay_image_ids.size(); i++) {
360
361
    // detect if 'iovl' is referencing itself
362
363
32
    if (m_overlay_image_ids[i] == get_id()) {
364
0
      return Error{heif_error_Invalid_input,
365
0
                   heif_suberror_Unspecified,
366
0
                   "Self-reference in 'iovl' image item."};
367
0
    }
368
369
32
    auto imgItem = get_context()->get_image(m_overlay_image_ids[i], true);
370
32
    if (!imgItem) {
371
15
      return Error(heif_error_Invalid_input, heif_suberror_Nonexisting_item_referenced, "'iovl' image references a non-existing item.");
372
15
    }
373
17
    if (auto error = imgItem->get_item_error()) {
374
8
      return error;
375
8
    }
376
377
9
    auto decodeResult = imgItem->decode_image(options, false, 0,0, decode_state);
378
9
    if (!decodeResult) {
379
9
      return decodeResult.error();
380
9
    }
381
382
0
    std::shared_ptr<HeifPixelImage> overlay_img = *decodeResult;
383
384
385
    // process overlay in RGB space
386
387
0
    if (overlay_img->get_colorspace() != heif_colorspace_RGB ||
388
0
        overlay_img->get_chroma_format() != heif_chroma_444) {
389
0
      auto overlay_img_result = convert_colorspace(overlay_img, heif_colorspace_RGB, heif_chroma_444,
390
0
                                                   nclx_profile::undefined(),
391
0
                                                   0, options.color_conversion_options, options.color_conversion_options_ext,
392
0
                                                   get_context()->get_security_limits());
393
0
      if (!overlay_img_result) {
394
0
        return overlay_img_result.error();
395
0
      }
396
0
      else {
397
0
        overlay_img = *overlay_img_result;
398
0
      }
399
0
    }
400
401
0
    int32_t dx, dy;
402
0
    m_overlay_spec.get_offset(i, &dx, &dy);
403
404
0
    err = img->overlay(overlay_img, dx, dy);
405
0
    if (err) {
406
0
      if (err.error_code == heif_error_Invalid_input &&
407
0
          err.sub_error_code == heif_suberror_Overlay_image_outside_of_canvas) {
408
        // NOP, ignore this error
409
0
      }
410
0
      else {
411
0
        return err;
412
0
      }
413
0
    }
414
0
  }
415
416
0
  return img;
417
32
}
418
419
420
int ImageItem_Overlay::get_luma_bits_per_pixel() const
421
44
{
422
44
  auto child_result = get_context()->find_first_coded_image_id(get_id());
423
44
  if (child_result.is_error()) {
424
23
    return -1;
425
23
  }
426
427
21
  auto image = get_context()->get_image(*child_result, true);
428
21
  return image->get_luma_bits_per_pixel();
429
44
}
430
431
432
int ImageItem_Overlay::get_chroma_bits_per_pixel() const
433
0
{
434
0
  auto child_result = get_context()->find_first_coded_image_id(get_id());
435
0
  if (child_result.is_error()) {
436
0
    return -1;
437
0
  }
438
439
0
  auto image = get_context()->get_image(*child_result, true);
440
0
  return image->get_chroma_bits_per_pixel();
441
0
}
442
443
444
Error ImageItem_Overlay::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
445
88
{
446
88
  *out_colorspace = heif_colorspace_RGB;
447
88
  *out_chroma = heif_chroma_444;
448
449
88
  return Error::Ok;
450
88
}
451
452
453
void ImageItem_Overlay::populate_component_descriptions()
454
239
{
455
239
  if (!get_component_descriptions().empty()) {
456
36
    return;
457
36
  }
458
459
  // The overlay is always composed in RGB 8-bit 4:4:4 onto the canvas
460
  // (decode_overlay_image converts each input child to RGB and uses an RGB
461
  // background color). So the description we publish reflects that fixed
462
  // output format, not the children's formats.
463
203
  uint32_t w = get_ispe_width();
464
203
  uint32_t h = get_ispe_height();
465
203
  if (w == 0 || h == 0) {
466
89
    return;
467
89
  }
468
469
342
  auto emit = [this, w, h](heif_channel ch, uint16_t type) {
470
342
    ComponentDescription d;
471
342
    d.component_id = mint_component_id();
472
342
    d.channel = ch;
473
342
    d.component_type = type;
474
342
    d.datatype = heif_component_datatype_unsigned_integer;
475
342
    d.bit_depth = 8;
476
342
    d.width = w;
477
342
    d.height = h;
478
342
    d.has_data_plane = true;
479
342
    add_component_description(std::move(d));
480
342
  };
481
482
114
  emit(heif_channel_R, heif_cmpd_component_type_red);
483
114
  emit(heif_channel_G, heif_cmpd_component_type_green);
484
114
  emit(heif_channel_B, heif_cmpd_component_type_blue);
485
114
}
486
487
488
Result<std::shared_ptr<ImageItem_Overlay>> ImageItem_Overlay::add_new_overlay_item(HeifContext* ctx, const ImageOverlay& overlayspec)
489
0
{
490
0
  if (overlayspec.get_num_offsets() > 0xFFFF) {
491
0
    return Error{heif_error_Usage_error,
492
0
                 heif_suberror_Unspecified,
493
0
                 "Too many overlay images (maximum: 65535)"};
494
0
  }
495
496
0
  std::vector<heif_item_id> ref_ids;
497
498
0
  auto file = ctx->get_heif_file();
499
500
0
  for (const auto& overlay : overlayspec.get_overlay_stack()) {
501
0
    file->get_infe_box(overlay.image_id)->set_hidden_item(true); // only show the full overlay
502
0
    ref_ids.push_back(overlay.image_id);
503
0
  }
504
505
506
  // Create ImageOverlay
507
508
0
  std::vector<uint8_t> iovl_data = overlayspec.write();
509
510
  // Create IOVL Item
511
512
0
  auto iovl_id_result = file->add_new_image(fourcc("iovl"));
513
0
  if (!iovl_id_result) {
514
0
    return iovl_id_result.error();
515
0
  }
516
0
  heif_item_id iovl_id = *iovl_id_result;
517
0
  std::shared_ptr<ImageItem_Overlay> iovl_image = std::make_shared<ImageItem_Overlay>(ctx, iovl_id);
518
0
  ctx->insert_image_item(iovl_id, iovl_image);
519
0
  const int construction_method = 1; // 0=mdat 1=idat
520
0
  file->append_iloc_data(iovl_id, iovl_data, construction_method);
521
522
  // Connect images to overlay
523
0
  file->add_iref_reference(iovl_id, fourcc("dimg"), ref_ids);
524
525
  // Add ISPE property
526
0
  auto ispe = std::make_shared<Box_ispe>();
527
0
  ispe->set_size(overlayspec.get_canvas_width(), overlayspec.get_canvas_height());
528
0
  iovl_image->add_property(ispe, false);
529
530
  // Add PIXI property (copy from first image) - According to MIAF, all images shall have the same color information.
531
0
  auto pixi = file->get_property_for_item<Box_pixi>(ref_ids[0]);
532
0
  iovl_image->add_property(pixi, true);
533
534
  // Set Brands
535
  //m_heif_file->set_brand(encoder->plugin->compression_format,
536
  //                       out_grid_image->is_miaf_compatible());
537
538
0
  return iovl_image;
539
0
}
540
541
heif_brand2 ImageItem_Overlay::get_compatible_brand() const
542
0
{
543
0
  if (m_overlay_image_ids.empty()) { return 0; }
544
545
0
  heif_item_id child_id = m_overlay_image_ids[0];
546
0
  auto child = get_context()->get_image(child_id, false);
547
0
  if (!child) { return 0; }
548
549
0
  return child->get_compatible_brand();
550
0
}