Coverage Report

Created: 2026-08-13 07:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libheif/libheif/image-items/tiled.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 "tiled.h"
22
#include "context.h"
23
#include "file.h"
24
#include <algorithm>
25
#include <limits>
26
#include "security_limits.h"
27
#include "codecs/hevc_dec.h"
28
#if WITH_UNCOMPRESSED_CODEC
29
#include "codecs/uncompressed/unc_boxes.h"
30
#endif
31
#include "api_structs.h"
32
33
34
static uint64_t readvec(const std::vector<uint8_t>& data, size_t& ptr, int len)
35
0
{
36
0
  uint64_t val = 0;
37
0
  while (len--) {
38
0
    val <<= 8;
39
0
    val |= data[ptr++];
40
0
  }
41
42
0
  return val;
43
0
}
44
45
46
Result<uint64_t> number_of_tiles(const heif_tiled_image_parameters& params, const heif_security_limits* limits)
47
0
{
48
0
  uint64_t nTiles = nTiles_h(params) * static_cast<uint64_t>(nTiles_v(params));
49
50
  // Enforce the limit before the extra-dimensions loop so it is checked
51
  // even when number_of_extra_dimensions == 0.
52
0
  if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) {
53
0
    return Error{
54
0
      heif_error_Unsupported_filetype,
55
0
      heif_suberror_Security_limit_exceeded,
56
0
      "Number of tiles exceeds security limit"
57
0
    };
58
0
  }
59
60
0
  for (int i = 0; i < params.number_of_extra_dimensions; i++) {
61
    // We only support up to 8 extra dimensions
62
0
    if (i == 8) {
63
0
      break;
64
0
    }
65
66
0
    if (params.extra_dimensions[i] == 0) {
67
0
      return Error{
68
0
        heif_error_Unsupported_filetype,
69
0
        heif_suberror_Unspecified,
70
0
        "Zero extra dimension size."
71
0
      };
72
0
    }
73
74
0
    if (nTiles > UINT64_MAX / params.extra_dimensions[i]) {
75
0
      return Error{
76
0
        heif_error_Unsupported_filetype,
77
0
        heif_suberror_Unspecified,
78
0
        "Number of tiles exceeds uint64 maximum."
79
0
      };
80
0
    }
81
82
0
    nTiles *= params.extra_dimensions[i];
83
84
0
    if (limits && limits->max_number_of_tiles && nTiles > limits->max_number_of_tiles) {
85
0
      return Error{
86
0
        heif_error_Unsupported_filetype,
87
0
        heif_suberror_Security_limit_exceeded,
88
0
        "Number of tiles exceeds security limit"
89
0
      };
90
0
    }
91
0
  }
92
93
0
  return nTiles;
94
0
}
95
96
97
uint32_t nTiles_h(const heif_tiled_image_parameters& params)
98
0
{
99
  // 64-bit arithmetic prevents wrap-around when image_width + tile_width - 1
100
  // exceeds UINT32_MAX. The quotient is bounded by image_width, so the
101
  // narrowing cast is safe. Callers are responsible for ensuring tile_width > 0.
102
0
  return static_cast<uint32_t>(
103
0
      (static_cast<uint64_t>(params.image_width) + params.tile_width - 1) / params.tile_width);
104
0
}
105
106
107
uint32_t nTiles_v(const heif_tiled_image_parameters& params)
108
0
{
109
0
  return static_cast<uint32_t>(
110
0
      (static_cast<uint64_t>(params.image_height) + params.tile_height - 1) / params.tile_height);
111
0
}
112
113
114
void Box_tilC::init_heif_tiled_image_parameters(heif_tiled_image_parameters& params)
115
252
{
116
252
  params.version = 1;
117
118
252
  params.image_width = 0;
119
252
  params.image_height = 0;
120
252
  params.tile_width = 0;
121
252
  params.tile_height = 0;
122
252
  params.compression_format_fourcc = 0;
123
252
  params.offset_field_length = 40;
124
252
  params.size_field_length = 24;
125
252
  params.number_of_extra_dimensions = 0;
126
127
2.01k
  for (uint32_t& dim : params.extra_dimensions) {
128
2.01k
    dim = 0;
129
2.01k
  }
130
131
252
  params.tiles_are_sequential = false;
132
252
}
133
134
135
void Box_tilC::derive_box_version()
136
0
{
137
0
  set_version(0);
138
139
0
  uint8_t flags = 0;
140
141
0
  switch (m_parameters.offset_field_length) {
142
0
    case 32:
143
0
      flags |= 0;
144
0
      break;
145
0
    case 40:
146
0
      flags |= 0x01;
147
0
      break;
148
0
    case 48:
149
0
      flags |= 0x02;
150
0
      break;
151
0
    case 64:
152
0
      flags |= 0x03;
153
0
      break;
154
0
    default:
155
0
      assert(false); // TODO: return error
156
0
  }
157
158
0
  switch (m_parameters.size_field_length) {
159
0
    case 0:
160
0
      flags |= 0;
161
0
      break;
162
0
    case 24:
163
0
      flags |= 0x04;
164
0
      break;
165
0
    case 32:
166
0
      flags |= 0x08;
167
0
      break;
168
0
    case 64:
169
0
      flags |= 0x0c;
170
0
      break;
171
0
    default:
172
0
      assert(false); // TODO: return error
173
0
  }
174
175
0
  if (m_parameters.tiles_are_sequential) {
176
0
    flags |= 0x10;
177
0
  }
178
179
0
  set_flags(flags);
180
0
}
181
182
183
Error Box_tilC::write(StreamWriter& writer) const
184
0
{
185
0
  assert(m_parameters.version == 1);
186
187
0
  size_t box_start = reserve_box_header_space(writer);
188
189
0
  if (m_parameters.number_of_extra_dimensions > 8) {
190
0
    assert(false); // currently not supported
191
0
  }
192
193
0
  writer.write32(m_parameters.tile_width);
194
0
  writer.write32(m_parameters.tile_height);
195
0
  writer.write32(m_parameters.compression_format_fourcc);
196
197
0
  writer.write8(m_parameters.number_of_extra_dimensions);
198
199
0
  for (int i = 0; i < m_parameters.number_of_extra_dimensions; i++) {
200
0
    writer.write32(m_parameters.extra_dimensions[i]);
201
0
  }
202
203
0
  auto& tile_properties = m_children;
204
0
  if (tile_properties.size() > 255) {
205
0
    return {heif_error_Encoding_error,
206
0
            heif_suberror_Unspecified,
207
0
            "Cannot write more than 255 tile properties in tilC header"};
208
0
  }
209
210
0
  writer.write8(static_cast<uint8_t>(tile_properties.size()));
211
0
  for (const auto& property : tile_properties) {
212
0
    property->write(writer);
213
0
  }
214
215
0
  prepend_header(writer, box_start);
216
217
0
  return Error::Ok;
218
0
}
219
220
221
std::string Box_tilC::dump(Indent& indent) const
222
0
{
223
0
  std::ostringstream sstr;
224
225
0
  sstr << BoxHeader::dump(indent);
226
227
0
  sstr << indent << "version: " << ((int) get_version()) << "\n"
228
       //<< indent << "image size: " << m_parameters.image_width << "x" << m_parameters.image_height << "\n"
229
0
       << indent << "tile size: " << m_parameters.tile_width << "x" << m_parameters.tile_height << "\n"
230
0
       << indent << "compression: " << fourcc_to_string(m_parameters.compression_format_fourcc) << "\n"
231
0
       << indent << "tiles are sequential: " << (m_parameters.tiles_are_sequential ? "yes" : "no") << "\n"
232
0
       << indent << "offset field length: " << ((int) m_parameters.offset_field_length) << " bits\n"
233
0
       << indent << "size field length: " << ((int) m_parameters.size_field_length) << " bits\n"
234
0
       << indent << "number of extra dimensions: " << ((int) m_parameters.number_of_extra_dimensions) << "\n";
235
236
0
  sstr << indent << "tile properties:\n"
237
0
       << dump_children(indent, true);
238
239
0
  return sstr.str();
240
241
0
}
242
243
244
Error Box_tilC::parse(BitstreamRange& range, const heif_security_limits* limits)
245
251
{
246
251
  parse_full_box_header(range);
247
248
  // Note: actually, we should allow 0 only, but there are a few images around that use version 1.
249
251
  if (get_version() > 1) {
250
4
    std::stringstream sstr;
251
4
    sstr << "'tili' image version " << ((int) get_version()) << " is not implemented yet";
252
253
4
    return {heif_error_Unsupported_feature,
254
4
            heif_suberror_Unsupported_data_version,
255
4
            sstr.str()};
256
4
  }
257
258
247
  m_parameters.version = get_version();
259
260
247
  uint32_t flags = get_flags();
261
262
247
  switch (flags & 0x03) {
263
174
    case 0:
264
174
      m_parameters.offset_field_length = 32;
265
174
      break;
266
9
    case 1:
267
9
      m_parameters.offset_field_length = 40;
268
9
      break;
269
45
    case 2:
270
45
      m_parameters.offset_field_length = 48;
271
45
      break;
272
19
    case 3:
273
19
      m_parameters.offset_field_length = 64;
274
19
      break;
275
247
  }
276
277
247
  switch (flags & 0x0c) {
278
204
    case 0x00:
279
204
      m_parameters.size_field_length = 0;
280
204
      break;
281
11
    case 0x04:
282
11
      m_parameters.size_field_length = 24;
283
11
      break;
284
6
    case 0x08:
285
6
      m_parameters.size_field_length = 32;
286
6
      break;
287
26
    case 0x0c:
288
26
      m_parameters.size_field_length = 64;
289
26
      break;
290
247
  }
291
292
247
  m_parameters.tiles_are_sequential = !!(flags & 0x10);
293
294
295
247
  m_parameters.tile_width = range.read32();
296
247
  m_parameters.tile_height = range.read32();
297
247
  m_parameters.compression_format_fourcc = range.read32();
298
299
247
  if (m_parameters.tile_width == 0 || m_parameters.tile_height == 0) {
300
4
    return {heif_error_Invalid_input,
301
4
            heif_suberror_Unspecified,
302
4
            "Tile with zero width or height."};
303
4
  }
304
305
306
  // --- extra dimensions
307
308
243
  m_parameters.number_of_extra_dimensions = range.read8();
309
310
715
  for (int i = 0; i < m_parameters.number_of_extra_dimensions; i++) {
311
513
    uint32_t size = range.read32();
312
313
513
    if (size == 0) {
314
41
      return {heif_error_Invalid_input,
315
41
              heif_suberror_Unspecified,
316
41
              "'tili' extra dimension may not be zero."};
317
41
    }
318
319
472
    if (i < 8) {
320
195
      m_parameters.extra_dimensions[i] = size;
321
195
    }
322
277
    else {
323
      // TODO: error: too many dimensions (not supported)
324
277
    }
325
472
  }
326
327
  // --- read tile properties
328
329
  // Check version for backwards compatibility with old format.
330
  // TODO: remove when spec is final and old test images have been converted
331
202
  if (get_version() == 0) {
332
157
    uint8_t num_properties = range.read8();
333
334
157
    Error error = read_children(range, num_properties, limits);
335
157
    if (error) {
336
22
      return error;
337
22
    }
338
157
  }
339
340
180
  return range.get_error();
341
202
}
342
343
344
Error TiledHeader::set_parameters(const heif_tiled_image_parameters& params)
345
0
{
346
0
  m_parameters = params;
347
348
0
  Result<uint64_t> num_tiles_result = number_of_tiles(params, heif_get_global_security_limits());
349
0
  if (auto err = num_tiles_result.error()) {
350
0
    return err;
351
0
  }
352
353
0
  m_offsets.resize(*num_tiles_result);
354
355
0
  for (auto& tile: m_offsets) {
356
0
    tile.offset = TILD_OFFSET_NOT_LOADED;
357
0
  }
358
359
0
  return Error::Ok;
360
0
}
361
362
363
Error TiledHeader::read_full_offset_table(const std::shared_ptr<HeifFile>& file, heif_item_id tild_id, const heif_security_limits* limits)
364
0
{
365
0
  Result<uint64_t> nTiles_result = number_of_tiles(m_parameters, limits);
366
0
  if (auto err = nTiles_result.error()) {
367
0
    return err;
368
0
  }
369
370
0
  return read_offset_table_range(file, tild_id, 0, *nTiles_result);
371
0
}
372
373
374
Error TiledHeader::read_offset_table_range(const std::shared_ptr<HeifFile>& file, heif_item_id tild_id,
375
                                           uint64_t start, uint64_t end)
376
0
{
377
0
  const Error eofError(heif_error_Invalid_input,
378
0
                       heif_suberror_Unspecified,
379
0
                       "Tili header data incomplete");
380
381
0
  std::vector<uint8_t> data;
382
383
384
385
  // --- load offsets
386
387
0
  size_t size_to_read = (end - start) * (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
388
0
  size_t start_offset = start * (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
389
390
  // TODO: when we request a file range from the stream reader, it may return a larger range.
391
  //       We should then also use this larger range to read more table entries.
392
  //       But this is not easy since our data may span several iloc extents and we have to map this back to item addresses.
393
  //       Maybe it is easier to just ignore the extra data and rely on the stream read to cache this data.
394
395
0
  Error err = file->append_data_from_iloc(tild_id, data, start_offset, size_to_read);
396
0
  if (err) {
397
0
    return err;
398
0
  }
399
400
  // Make sure we actually received as much data as we are about to parse. The
401
  // returned buffer may be shorter than requested (e.g. truncated iloc/idat
402
  // extents), and readvec() does not bounds-check its input.
403
0
  if (data.size() < size_to_read) {
404
0
    return eofError;
405
0
  }
406
407
0
  size_t idx = 0;
408
0
  for (uint64_t i = start; i < end; i++) {
409
0
    m_offsets[i].offset = readvec(data, idx, m_parameters.offset_field_length / 8);
410
411
0
    if (m_parameters.size_field_length) {
412
0
      assert(m_parameters.size_field_length <= 32);
413
0
      m_offsets[i].size = static_cast<uint32_t>(readvec(data, idx, m_parameters.size_field_length / 8));
414
0
    }
415
416
    // printf("[%zu] : offset/size: %zu %d\n", i, m_offsets[i].offset, m_offsets[i].size);
417
0
  }
418
419
0
  return Error::Ok;
420
0
}
421
422
423
size_t TiledHeader::get_header_size() const
424
0
{
425
0
  assert(m_header_size);
426
0
  return m_header_size;
427
0
}
428
429
430
uint32_t TiledHeader::get_offset_table_entry_size() const
431
0
{
432
0
  return (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
433
0
}
434
435
436
std::pair<uint32_t, uint32_t> TiledHeader::get_tile_offset_table_range_to_read(uint32_t idx, uint32_t nEntries) const
437
0
{
438
  // Defense in depth: callers are expected to validate idx, but if they don't,
439
  // returning an empty range prevents the subsequent read_offset_table_range
440
  // from writing past m_offsets.
441
0
  if (idx >= m_offsets.size()) {
442
0
    return {0, 0};
443
0
  }
444
445
0
  uint32_t start = idx;
446
0
  uint32_t end = idx+1;
447
448
0
  while (end < m_offsets.size() && end - idx < nEntries && m_offsets[end].offset == TILD_OFFSET_NOT_LOADED) {
449
0
    end++;
450
0
  }
451
452
0
  while (start > 0 && idx - start < nEntries && m_offsets[start-1].offset == TILD_OFFSET_NOT_LOADED) {
453
0
    start--;
454
0
  }
455
456
  // try to fill the smaller hole
457
458
0
  if (end - start > nEntries) {
459
0
    if (idx - start < end - idx) {
460
0
      end = start + nEntries;
461
0
    }
462
0
    else {
463
0
      start = end - nEntries;
464
0
    }
465
0
  }
466
467
0
  return {start, end};
468
0
}
469
470
471
Error TiledHeader::set_tild_tile_range(uint32_t tile_x, uint32_t tile_y, uint64_t offset, uint32_t size)
472
0
{
473
  // Offset and size are written into bit-fields of the configured widths;
474
  // silently truncating here produces files where the offset table points to
475
  // garbage. Reject the value so the caller knows to widen the fields.
476
0
  uint8_t off_bits = m_parameters.offset_field_length;
477
0
  uint8_t sz_bits  = m_parameters.size_field_length;
478
479
0
  if (off_bits < 64 && offset >> off_bits) {
480
0
    std::stringstream sstr;
481
0
    sstr << "Tile offset " << offset << " does not fit in the configured "
482
0
         << static_cast<int>(off_bits) << "-bit offset field. Use a wider "
483
0
            "offset_field_length (40/48/64) when encoding the tili image.";
484
0
    return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
485
0
  }
486
487
0
  if (sz_bits != 0 && sz_bits < 32 && size >> sz_bits) {
488
0
    std::stringstream sstr;
489
0
    sstr << "Tile size " << size << " does not fit in the configured "
490
0
         << static_cast<int>(sz_bits) << "-bit size field.";
491
0
    return {heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
492
0
  }
493
494
0
  uint64_t idx = uint64_t{tile_y} * nTiles_h(m_parameters) + tile_x;
495
0
  m_offsets[idx].offset = offset;
496
0
  m_offsets[idx].size = size;
497
0
  return Error::Ok;
498
0
}
499
500
501
template<typename I>
502
void writevec(uint8_t* data, size_t& idx, I value, int len)
503
0
{
504
0
  for (int i = 0; i < len; i++) {
505
0
    data[idx + i] = static_cast<uint8_t>((value >> (len - 1 - i) * 8) & 0xFF);
506
0
  }
507
508
0
  idx += len;
509
0
}
510
511
512
Result<std::vector<uint8_t>> TiledHeader::write_offset_table()
513
0
{
514
0
  Result<uint64_t> nTiles_result = number_of_tiles(m_parameters, nullptr);
515
0
  if (auto err = nTiles_result.error()) {
516
0
    return err;
517
0
  }
518
519
520
0
  int offset_entry_size = (m_parameters.offset_field_length + m_parameters.size_field_length) / 8;
521
0
  uint64_t size = *nTiles_result * offset_entry_size;
522
523
0
  std::vector<uint8_t> data;
524
0
  data.resize(size);
525
526
0
  size_t idx = 0;
527
528
0
  uint8_t off_bits = m_parameters.offset_field_length;
529
0
  uint8_t sz_bits  = m_parameters.size_field_length;
530
531
0
  for (const auto& offset: m_offsets) {
532
0
    if (off_bits < 64 && offset.offset >> off_bits) {
533
0
      std::stringstream sstr;
534
0
      sstr << "Tile offset " << offset.offset << " does not fit in the "
535
0
              "configured " << static_cast<int>(off_bits) << "-bit offset field.";
536
0
      return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
537
0
    }
538
0
    if (sz_bits != 0 && sz_bits < 32 && offset.size >> sz_bits) {
539
0
      std::stringstream sstr;
540
0
      sstr << "Tile size " << offset.size << " does not fit in the "
541
0
              "configured " << static_cast<int>(sz_bits) << "-bit size field.";
542
0
      return Error{heif_error_Encoding_error, heif_suberror_Unspecified, sstr.str()};
543
0
    }
544
545
0
    writevec(data.data(), idx, offset.offset, m_parameters.offset_field_length / 8);
546
547
0
    if (m_parameters.size_field_length != 0) {
548
0
      writevec(data.data(), idx, offset.size, m_parameters.size_field_length / 8);
549
0
    }
550
0
  }
551
552
0
  assert(idx == data.size());
553
554
0
  m_header_size = data.size();
555
556
0
  return data;
557
0
}
558
559
560
std::string TiledHeader::dump() const
561
0
{
562
0
  std::stringstream sstr;
563
564
0
  sstr << "offsets: ";
565
566
  // TODO
567
568
0
  for (const auto& offset: m_offsets) {
569
0
    sstr << offset.offset << ", size: " << offset.size << "\n";
570
0
  }
571
572
0
  return sstr.str();
573
0
}
574
575
576
ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx)
577
0
        : ImageItem(ctx)
578
0
{
579
0
  m_tile_encoding_options = heif_encoding_options_alloc();
580
0
}
581
582
583
ImageItem_Tiled::ImageItem_Tiled(HeifContext* ctx, heif_item_id id)
584
2
        : ImageItem(ctx, id)
585
2
{
586
2
  m_tile_encoding_options = heif_encoding_options_alloc();
587
2
}
588
589
590
ImageItem_Tiled::~ImageItem_Tiled()
591
2
{
592
2
  heif_encoding_options_free(m_tile_encoding_options);
593
2
}
594
595
596
heif_compression_format ImageItem_Tiled::get_compression_format() const
597
0
{
598
0
  return compression_format_from_fourcc_infe_type(m_tild_header.get_parameters().compression_format_fourcc);
599
0
}
600
601
602
Error ImageItem_Tiled::initialize_decoder()
603
2
{
604
2
  auto heif_file = get_context()->get_heif_file();
605
606
2
  auto tilC_box = get_property<Box_tilC>();
607
2
  if (!tilC_box) {
608
2
    return {heif_error_Invalid_input,
609
2
            heif_suberror_Unspecified,
610
2
            "Tiled image without 'tilC' property box."};
611
2
  }
612
613
0
  auto ispe_box = get_property<Box_ispe>();
614
0
  if (!ispe_box) {
615
0
    return {heif_error_Invalid_input,
616
0
            heif_suberror_Unspecified,
617
0
            "Tiled image without 'ispe' property box."};
618
0
  }
619
620
0
  heif_tiled_image_parameters parameters = tilC_box->get_parameters();
621
0
  parameters.image_width = ispe_box->get_width();
622
0
  parameters.image_height = ispe_box->get_height();
623
624
0
  if (parameters.image_width == 0 || parameters.image_height == 0) {
625
0
    return {heif_error_Invalid_input,
626
0
            heif_suberror_Unspecified,
627
0
            "'tili' image with zero width or height."};
628
0
  }
629
630
0
  if (Error err = m_tild_header.set_parameters(parameters)) {
631
0
    return err;
632
0
  }
633
634
635
  // --- create a dummy image item for decoding tiles
636
637
0
  heif_compression_format format = compression_format_from_fourcc_infe_type(m_tild_header.get_parameters().compression_format_fourcc);
638
0
  m_tile_item = ImageItem::alloc_for_compression_format(get_context(), format);
639
640
  // For backwards compatibility: copy over properties from `tili` item.
641
  // TODO: remove when spec is final and old test images have been converted
642
0
  if (tilC_box->get_version() == 1) {
643
0
    auto propertiesResult = get_properties();
644
0
    if (!propertiesResult) {
645
0
      return propertiesResult.error();
646
0
    }
647
648
    // Filter out per-tile boxes incompatible with tili's shared template
649
0
    auto props = *propertiesResult;
650
0
#if WITH_UNCOMPRESSED_CODEC
651
0
    for (const auto& box : props) {
652
0
      if (box->get_short_type() == fourcc("icef")) {
653
0
        auto icef = std::dynamic_pointer_cast<Box_icef>(box);
654
0
        if (icef && icef->get_units().size() > 1) {
655
0
          return {heif_error_Invalid_input,
656
0
                  heif_suberror_Unspecified,
657
0
                  "icef box with multiple units is incompatible with tili shared tile template."};
658
0
        }
659
0
      }
660
0
    }
661
0
    std::erase_if(props, [](const std::shared_ptr<Box>& box) {
662
0
      uint32_t type = box->get_short_type();
663
0
      return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc");
664
0
    });
665
0
#endif
666
667
0
    m_tile_item->set_properties(props);
668
0
  }
669
0
  else {
670
    // --- This is the new method
671
672
    // Synthesize an ispe box if there was none in the file
673
674
0
    auto tile_properties = tilC_box->get_all_child_boxes();
675
676
    // Filter out per-tile boxes incompatible with tili's shared template
677
0
#if WITH_UNCOMPRESSED_CODEC
678
0
    for (const auto& box : tile_properties) {
679
0
      if (box->get_short_type() == fourcc("icef")) {
680
0
        auto icef = std::dynamic_pointer_cast<Box_icef>(box);
681
0
        if (icef && icef->get_units().size() > 1) {
682
0
          return {heif_error_Invalid_input,
683
0
                  heif_suberror_Unspecified,
684
0
                  "icef box with multiple units is incompatible with tili shared tile template."};
685
0
        }
686
0
      }
687
0
    }
688
0
    std::erase_if(tile_properties, [](const std::shared_ptr<Box>& box) {
689
0
      uint32_t type = box->get_short_type();
690
0
      return type == fourcc("icef") || type == fourcc("sbpm") || type == fourcc("snuc");
691
0
    });
692
0
#endif
693
694
0
    bool have_ispe = false;
695
0
    for (const auto& property : tile_properties) {
696
0
      if (property->get_short_type() == fourcc("ispe")) {
697
0
        have_ispe = true;
698
0
        break;
699
0
      }
700
0
    }
701
702
0
    if (!have_ispe) {
703
0
      auto ispe = std::make_shared<Box_ispe>();
704
0
      ispe->set_size(parameters.tile_width, parameters.tile_height);
705
0
      tile_properties.emplace_back(std::move(ispe));
706
0
    }
707
708
0
    m_tile_item->set_properties(tile_properties);
709
0
  }
710
711
0
  m_tile_decoder = Decoder::alloc_for_infe_type(m_tile_item.get());
712
0
  if (!m_tile_decoder) {
713
0
    return {heif_error_Unsupported_feature,
714
0
            heif_suberror_Unsupported_codec,
715
0
            "'tili' image with unsupported compression format."};
716
0
  }
717
718
0
  if (m_preload_offset_table) {
719
0
    if (Error err = m_tild_header.read_full_offset_table(heif_file, get_id(), get_context()->get_security_limits())) {
720
0
      return err;
721
0
    }
722
0
  }
723
724
725
0
  return Error::Ok;
726
0
}
727
728
729
void ImageItem_Tiled::populate_component_descriptions()
730
2
{
731
  // Idempotent: skip if already populated.
732
2
  if (!get_component_descriptions().empty()) {
733
0
    return;
734
0
  }
735
736
  // initialize_decoder() must have run; m_tile_item carries the per-tile
737
  // properties (cmpd/uncC for unci tiles, codec config for visual tiles)
738
  // and its own populate has filled tile-sized component descriptions.
739
2
  if (!m_tile_item) {
740
2
    return;
741
2
  }
742
743
0
  uint32_t tile_w = m_tild_header.get_parameters().tile_width;
744
0
  uint32_t tile_h = m_tild_header.get_parameters().tile_height;
745
0
  populate_descriptions_from_child(*m_tile_item, tile_w, tile_h);
746
0
}
747
748
749
Result<std::shared_ptr<ImageItem_Tiled>>
750
ImageItem_Tiled::add_new_tiled_item(HeifContext* ctx, const heif_tiled_image_parameters* parameters,
751
                                    const heif_encoder* encoder,
752
                                    const heif_encoding_options* encoding_options)
753
0
{
754
0
  Result<uint64_t> num_tiles_result = number_of_tiles(*parameters, ctx->get_security_limits());
755
0
  if (auto err = num_tiles_result.error()) {
756
0
    return err;
757
0
  }
758
759
  // Create 'tili' Item
760
761
0
  auto file = ctx->get_heif_file();
762
763
0
  auto tild_id_result = ctx->get_heif_file()->add_new_image(fourcc("tili"));
764
0
  if (!tild_id_result) {
765
0
    return tild_id_result.error();
766
0
  }
767
0
  heif_item_id tild_id = *tild_id_result;
768
0
  auto tild_image = std::make_shared<ImageItem_Tiled>(ctx, tild_id);
769
0
  tild_image->set_resolution(parameters->image_width, parameters->image_height);
770
0
  ctx->insert_image_item(tild_id, tild_image);
771
772
0
  if (encoding_options) {
773
    // encoding options for the tiles, but do not apply transformative properties
774
0
    heif_encoding_options_copy(tild_image->m_tile_encoding_options, encoding_options);
775
0
    tild_image->m_tile_encoding_options->image_orientation = heif_orientation_normal;
776
777
    // orientation of the main image
778
0
    tild_image->m_image_orientation = encoding_options->image_orientation;
779
0
  }
780
781
  // Create tilC box
782
783
0
  auto tilC_box = std::make_shared<Box_tilC>();
784
0
  tilC_box->set_parameters(*parameters);
785
0
  tilC_box->set_compression_format(encoder->plugin->compression_format);
786
0
  tild_image->add_property(tilC_box, true);
787
788
  // Create header + offset table
789
790
0
  TiledHeader tild_header;
791
0
  tild_header.set_parameters(*parameters);
792
0
  tild_header.set_compression_format(encoder->plugin->compression_format);
793
794
0
  Result<std::vector<uint8_t>> header_data_result = tild_header.write_offset_table();
795
0
  if (auto err = header_data_result.error()) {
796
0
    return err;
797
0
  }
798
799
0
  const int construction_method = 0; // 0=mdat 1=idat
800
0
  file->append_iloc_data(tild_id, *header_data_result, construction_method);
801
802
803
0
  if (parameters->image_width > 0xFFFFFFFF || parameters->image_height > 0xFFFFFFFF) {
804
0
    return {Error(heif_error_Usage_error, heif_suberror_Invalid_image_size,
805
0
                  "'ispe' only supports image sized up to 4294967295 pixels per dimension")};
806
0
  }
807
808
  // Add ISPE property
809
0
  auto ispe = std::make_shared<Box_ispe>();
810
0
  ispe->set_size(static_cast<uint32_t>(parameters->image_width),
811
0
                 static_cast<uint32_t>(parameters->image_height));
812
0
  tild_image->add_property(ispe, true);
813
814
#if 0
815
  // TODO
816
817
  // Add PIXI property (copy from first tile)
818
  auto pixi = m_heif_file->get_property<Box_pixi>(tile_ids[0]);
819
  m_heif_file->add_property(grid_id, pixi, true);
820
#endif
821
822
0
  tild_image->set_tild_header(tild_header);
823
0
  tild_image->set_next_tild_position(header_data_result->size());
824
825
  // Set Brands
826
  //m_heif_file->set_brand(encoder->plugin->compression_format,
827
  //                       out_grid_image->is_miaf_compatible());
828
829
0
  return {tild_image};
830
0
}
831
832
833
Error ImageItem_Tiled::add_image_tile(uint32_t tile_x, uint32_t tile_y,
834
                                     const std::shared_ptr<HeifPixelImage>& image,
835
                                     heif_encoder* encoder)
836
0
{
837
0
  auto item = ImageItem::alloc_for_compression_format(get_context(), encoder->plugin->compression_format);
838
839
0
  Result<std::shared_ptr<HeifPixelImage>> colorConversionResult;
840
0
  colorConversionResult = item->get_encoder()->convert_colorspace_for_encoding(image, encoder,
841
0
                                                                               m_tile_encoding_options->output_nclx_profile,
842
0
                                                                               &m_tile_encoding_options->color_conversion_options,
843
0
                                                                               get_context()->get_security_limits());
844
0
  if (!colorConversionResult) {
845
0
    return colorConversionResult.error();
846
0
  }
847
848
0
  std::shared_ptr<HeifPixelImage> colorConvertedImage = *colorConversionResult;
849
850
0
  Result<Encoder::CodedImageData> encodeResult = item->encode_to_bitstream_and_boxes(colorConvertedImage, encoder, *m_tile_encoding_options, heif_image_input_class_normal);
851
852
0
  if (!encodeResult) {
853
0
    return encodeResult.error();
854
0
  }
855
856
0
  const int construction_method = 0; // 0=mdat 1=idat
857
0
  get_file()->append_iloc_data(get_id(), encodeResult->bitstream, construction_method);
858
859
0
  auto& header = m_tild_header;
860
861
0
  if (image->get_width() != header.get_parameters().tile_width ||
862
0
      image->get_height() != header.get_parameters().tile_height) {
863
0
    return {heif_error_Usage_error,
864
0
            heif_suberror_Unspecified,
865
0
            "Tile image size does not match the specified tile size."};
866
0
  }
867
868
0
  uint64_t offset = get_next_tild_position();
869
0
  size_t dataSize = encodeResult->bitstream.size();
870
0
  if (dataSize > 0xFFFFFFFF) {
871
0
    return {heif_error_Encoding_error, heif_suberror_Unspecified, "Compressed tile size exceeds maximum tile size."};
872
0
  }
873
0
  if (Error err = header.set_tild_tile_range(tile_x, tile_y, offset, static_cast<uint32_t>(dataSize))) {
874
0
    return err;
875
0
  }
876
0
  set_next_tild_position(offset + encodeResult->bitstream.size());
877
878
0
  auto tilC = get_property<Box_tilC>();
879
0
  assert(tilC);
880
881
0
  std::vector<std::shared_ptr<Box>>& tile_properties = tilC->get_tile_properties();
882
883
0
  for (auto& propertyBox : encodeResult->properties) {
884
885
    // we do not have to save ispe boxes in the tile properties as this is automatically synthesized
886
887
0
    if (propertyBox->get_short_type() == fourcc("ispe")) {
888
0
      continue;
889
0
    }
890
891
0
#if WITH_UNCOMPRESSED_CODEC
892
    // icef/sbpm/snuc contain per-tile data incompatible with tili's shared tile template
893
0
    uint32_t ptype = propertyBox->get_short_type();
894
895
0
    if (ptype == fourcc("icef")) {
896
0
      auto icef = std::dynamic_pointer_cast<Box_icef>(propertyBox);
897
0
      if (icef && icef->get_units().size() > 1) {
898
0
        return {heif_error_Usage_error,
899
0
                heif_suberror_Unspecified,
900
0
                "icef box with multiple units is incompatible with tili shared tile template."};
901
0
      }
902
      // Single-unit icef can be safely skipped
903
0
      continue;
904
0
    }
905
906
0
    if (ptype == fourcc("sbpm") || ptype == fourcc("snuc")) {
907
0
      return {heif_error_Usage_error,
908
0
              heif_suberror_Unspecified,
909
0
              "Cannot store per-tile property (" + fourcc_to_string(ptype) + ") in tili shared tile template."};
910
0
    }
911
0
#endif
912
913
    // skip properties that exist already
914
915
0
    bool exists = std::any_of(tile_properties.begin(),
916
0
                              tile_properties.end(),
917
0
                              [&propertyBox](const std::shared_ptr<Box>& p) { return p->get_short_type() == propertyBox->get_short_type();});
918
0
    if (exists) {
919
0
      continue;
920
0
    }
921
922
0
    tile_properties.emplace_back(propertyBox);
923
924
    // some tile properties are also added to the tili image
925
926
0
    switch (propertyBox->get_short_type()) {
927
0
      case fourcc("pixi"):
928
0
        get_file()->add_property(get_id(), propertyBox, propertyBox->is_essential());
929
0
        break;
930
0
    }
931
932
0
    get_file()->add_orientation_properties(get_id(), m_image_orientation);
933
0
  }
934
935
  //get_file()->set_brand(encoder->plugin->compression_format,
936
  //                      true); // TODO: out_grid_image->is_miaf_compatible());
937
938
0
  return Error::Ok;
939
0
}
940
941
942
Error ImageItem_Tiled::process_before_write()
943
0
{
944
  // overwrite offsets
945
946
0
  const int construction_method = 0; // 0=mdat 1=idat
947
948
0
  Result<std::vector<uint8_t>> header_data_result = m_tild_header.write_offset_table();
949
0
  if (auto err = header_data_result.error()) {
950
0
    return err;
951
0
  }
952
953
0
  get_file()->replace_iloc_data(get_id(), 0, *header_data_result, construction_method);
954
0
  return {};
955
0
}
956
957
958
Result<std::shared_ptr<HeifPixelImage>>
959
ImageItem_Tiled::decode_compressed_image(const heif_decoding_options& options,
960
                                         bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
961
                                         std::set<heif_item_id> processed_ids) const
962
0
{
963
0
  if (decode_tile_only) {
964
0
    return decode_grid_tile(options, tile_x0, tile_y0);
965
0
  }
966
0
  else {
967
0
    return Error{heif_error_Unsupported_feature, heif_suberror_Unspecified,
968
0
                 "'tili' images can only be accessed per tile"};
969
0
  }
970
0
}
971
972
973
Error ImageItem_Tiled::append_compressed_tile_data(std::vector<uint8_t>& data, uint32_t tx, uint32_t ty) const
974
0
{
975
0
  uint64_t idx64 = static_cast<uint64_t>(ty) * nTiles_h(m_tild_header.get_parameters()) + tx;
976
0
  if (idx64 >= m_tild_header.get_num_tiles()) {
977
0
    return Error{heif_error_Invalid_input,
978
0
                 heif_suberror_Unspecified,
979
0
                 "Tile index out of range."};
980
0
  }
981
0
  auto idx = static_cast<uint32_t>(idx64);
982
983
0
  if (!m_tild_header.is_tile_offset_known(idx)) {
984
0
    Error err = const_cast<ImageItem_Tiled*>(this)->load_tile_offset_entry(idx);
985
0
    if (err) {
986
0
      return err;
987
0
    }
988
0
  }
989
990
0
  uint64_t offset = m_tild_header.get_tile_offset(idx);
991
0
  uint64_t size = m_tild_header.get_tile_size(idx);
992
993
0
  Error err = get_file()->append_data_from_iloc(get_id(), data, offset, size);
994
0
  if (err.error_code) {
995
0
    return err;
996
0
  }
997
998
0
  return Error::Ok;
999
0
}
1000
1001
1002
Result<DataExtent>
1003
ImageItem_Tiled::get_compressed_data_for_tile(uint32_t tx, uint32_t ty) const
1004
0
{
1005
  // --- get compressed data
1006
1007
0
  Error err = m_tile_item->initialize_decoder();
1008
0
  if (err) {
1009
0
    return err;
1010
0
  }
1011
1012
0
  Result<std::vector<uint8_t>> dataResult = m_tile_item->read_bitstream_configuration_data();
1013
0
  if (!dataResult) {
1014
0
    return dataResult.error();
1015
0
  }
1016
1017
0
  std::vector<uint8_t> data = std::move(*dataResult);
1018
0
  err = append_compressed_tile_data(data, tx, ty);
1019
0
  if (err) {
1020
0
    return err;
1021
0
  }
1022
1023
  // --- decode
1024
1025
0
  DataExtent extent;
1026
0
  extent.m_raw = std::move(data);
1027
1028
0
  return std::move(extent);
1029
0
}
1030
1031
1032
Result<std::shared_ptr<HeifPixelImage>>
1033
ImageItem_Tiled::decode_grid_tile(const heif_decoding_options& options, uint32_t tx, uint32_t ty) const
1034
0
{
1035
0
  Result<DataExtent> extentResult = get_compressed_data_for_tile(tx, ty);
1036
0
  if (!extentResult) {
1037
0
    return extentResult.error();
1038
0
  }
1039
1040
0
  m_tile_decoder->set_data_extent(std::move(*extentResult));
1041
1042
0
  uint32_t tw = 0, th = 0;
1043
0
  get_tile_size(tw, th);
1044
0
  heif_security_limits tightened = tighten_image_size_limit_for_ispe(
1045
0
      get_context()->get_security_limits(), tw, th,
1046
0
      max_coding_unit_size_for_codec(m_tile_decoder->get_compression_format()));
1047
1048
0
  return m_tile_decoder->decode_single_frame_from_compressed_data(options, &tightened);
1049
0
}
1050
1051
1052
Error ImageItem_Tiled::load_tile_offset_entry(uint32_t idx)
1053
0
{
1054
0
  uint32_t nEntries = mReadChunkSize_bytes / m_tild_header.get_offset_table_entry_size();
1055
0
  std::pair<uint32_t, uint32_t> range = m_tild_header.get_tile_offset_table_range_to_read(idx, nEntries);
1056
1057
0
  return m_tild_header.read_offset_table_range(get_file(), get_id(), range.first, range.second);
1058
0
}
1059
1060
1061
heif_image_tiling ImageItem_Tiled::get_heif_image_tiling() const
1062
0
{
1063
0
  heif_image_tiling tiling{};
1064
1065
0
  tiling.num_columns = nTiles_h(m_tild_header.get_parameters());
1066
0
  tiling.num_rows = nTiles_v(m_tild_header.get_parameters());
1067
1068
0
  tiling.tile_width = m_tild_header.get_parameters().tile_width;
1069
0
  tiling.tile_height = m_tild_header.get_parameters().tile_height;
1070
1071
0
  tiling.image_width = m_tild_header.get_parameters().image_width;
1072
0
  tiling.image_height = m_tild_header.get_parameters().image_height;
1073
0
  tiling.number_of_extra_dimensions = m_tild_header.get_parameters().number_of_extra_dimensions;
1074
0
  for (int i = 0; i < std::min(tiling.number_of_extra_dimensions, uint8_t(8)); i++) {
1075
0
    tiling.extra_dimension_size[i] = m_tild_header.get_parameters().extra_dimensions[i];
1076
0
  }
1077
1078
0
  return tiling;
1079
0
}
1080
1081
1082
void ImageItem_Tiled::get_tile_size(uint32_t& w, uint32_t& h) const
1083
0
{
1084
0
  w = m_tild_header.get_parameters().tile_width;
1085
0
  h = m_tild_header.get_parameters().tile_height;
1086
0
}
1087
1088
1089
Error ImageItem_Tiled::get_coded_image_colorspace(heif_colorspace* out_colorspace, heif_chroma* out_chroma) const
1090
0
{
1091
0
  uint32_t tx=0, ty=0; // TODO: find a tile that is defined.
1092
1093
0
  Result<DataExtent> extentResult = get_compressed_data_for_tile(tx, ty);
1094
0
  if (!extentResult) {
1095
0
    return extentResult.error();
1096
0
  }
1097
1098
0
  m_tile_decoder->set_data_extent(std::move(*extentResult));
1099
1100
0
  Error err = m_tile_decoder->get_coded_image_colorspace(out_colorspace, out_chroma);
1101
0
  if (err) {
1102
0
    return err;
1103
0
  }
1104
1105
0
  postprocess_coded_image_colorspace(out_colorspace, out_chroma);
1106
1107
0
  return Error::Ok;
1108
0
}
1109
1110
1111
int ImageItem_Tiled::get_luma_bits_per_pixel() const
1112
0
{
1113
0
  DataExtent any_tile_extent;
1114
0
  append_compressed_tile_data(any_tile_extent.m_raw, 0,0); // TODO: use tile that is already loaded
1115
0
  m_tile_decoder->set_data_extent(std::move(any_tile_extent));
1116
1117
0
  return m_tile_decoder->get_luma_bits_per_pixel();
1118
0
}
1119
1120
int ImageItem_Tiled::get_chroma_bits_per_pixel() const
1121
0
{
1122
0
  DataExtent any_tile_extent;
1123
0
  append_compressed_tile_data(any_tile_extent.m_raw, 0,0); // TODO: use tile that is already loaded
1124
0
  m_tile_decoder->set_data_extent(std::move(any_tile_extent));
1125
1126
0
  return m_tile_decoder->get_chroma_bits_per_pixel();
1127
0
}
1128
1129
heif_brand2 ImageItem_Tiled::get_compatible_brand() const
1130
0
{
1131
0
  return 0;
1132
1133
  // TODO: it is not clear to me what brand to use here.
1134
1135
  /*
1136
  switch (m_tild_header.get_parameters().compression_format_fourcc) {
1137
    case heif_compression_HEVC:
1138
      return heif_brand2_heic;
1139
  }
1140
   */
1141
0
}