Coverage Report

Created: 2026-06-13 08:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/extras/enc/npy.cc
Line
Count
Source
1
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2
//
3
// Use of this source code is governed by a BSD-style
4
// license that can be found in the LICENSE file.
5
6
#include "lib/extras/enc/npy.h"
7
8
#include <jxl/codestream_header.h>
9
#include <jxl/types.h>
10
11
#include <cstddef>
12
#include <cstdint>
13
#include <cstring>
14
#include <memory>
15
#include <ostream>
16
#include <sstream>
17
#include <string>
18
#include <type_traits>
19
#include <utility>
20
#include <vector>
21
22
#include "lib/extras/enc/encode.h"
23
#include "lib/extras/packed_image.h"
24
#include "lib/jxl/base/common.h"
25
#include "lib/jxl/base/data_parallel.h"
26
#include "lib/jxl/base/status.h"
27
28
namespace jxl {
29
namespace extras {
30
namespace {
31
32
// JSON value writing
33
34
class JSONField {
35
 public:
36
0
  virtual ~JSONField() = default;
37
  virtual void Write(std::ostream& o, uint32_t indent) const = 0;
38
39
 protected:
40
0
  JSONField() = default;
41
};
42
43
class JSONValue : public JSONField {
44
 public:
45
  template <typename T>
46
0
  explicit JSONValue(const T& value) : value_(std::to_string(value)) {}
Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<float>(float const&)
Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<unsigned int>(unsigned int const&)
Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<int>(int const&)
47
48
0
  explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {}
49
50
0
  explicit JSONValue(bool value) : value_(value ? "true" : "false") {}
51
52
0
  void Write(std::ostream& o, uint32_t indent) const override { o << value_; }
53
54
 private:
55
  std::string value_;
56
};
57
58
class JSONDict : public JSONField {
59
 public:
60
0
  JSONDict() = default;
61
62
  template <typename T>
63
0
  T* AddEmpty(const std::string& key) {
64
0
    static_assert(std::is_convertible<T*, JSONField*>::value,
65
0
                  "T must be a JSONField");
66
0
    T* ret = new T();
67
0
    JSONField* field = static_cast<JSONField*>(ret);
68
0
    auto handle = std::unique_ptr<JSONField>(field);
69
0
    values_.emplace_back(key, std::move(handle));
70
0
    return ret;
71
0
  }
Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONArray* jxl::extras::(anonymous namespace)::JSONDict::AddEmpty<jxl::extras::(anonymous namespace)::JSONArray>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONDict* jxl::extras::(anonymous namespace)::JSONDict::AddEmpty<jxl::extras::(anonymous namespace)::JSONDict>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
72
73
  template <typename T>
74
0
  void Add(const std::string& key, const T& value) {
75
0
    JSONField* field = static_cast<JSONField*>(new JSONValue(value));
76
0
    auto handle = std::unique_ptr<JSONField>(field);
77
0
    values_.emplace_back(key, std::move(handle));
78
0
  }
Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<jxl::extras::(anonymous namespace)::JSONValue>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, jxl::extras::(anonymous namespace)::JSONValue const&)
Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<float>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, float const&)
Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<int>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int const&)
79
80
0
  void Write(std::ostream& o, uint32_t indent) const override {
81
0
    std::string indent_str(indent, ' ');
82
0
    o << "{";
83
0
    bool is_first = true;
84
0
    for (const auto& key_value : values_) {
85
0
      if (!is_first) {
86
0
        o << ",";
87
0
      }
88
0
      is_first = false;
89
0
      o << "\n" << indent_str << "  \"" << key_value.first << "\": ";
90
0
      key_value.second->Write(o, indent + 2);
91
0
    }
92
0
    if (!values_.empty()) {
93
0
      o << "\n" << indent_str;
94
0
    }
95
0
    o << "}";
96
0
  }
97
98
 private:
99
  // Dictionary with order.
100
  std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_;
101
};
102
103
class JSONArray : public JSONField {
104
 public:
105
0
  JSONArray() = default;
106
107
  template <typename T>
108
0
  T* AddEmpty() {
109
0
    static_assert(std::is_convertible<T*, JSONField*>::value,
110
0
                  "T must be a JSONField");
111
0
    T* ret = new T();
112
0
    values_.emplace_back(ret);
113
0
    return ret;
114
0
  }
115
116
  template <typename T>
117
0
  void Add(const T& value) {
118
0
    values_.emplace_back(new JSONValue(value));
119
0
  }
Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONArray::Add<unsigned int>(unsigned int const&)
Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONArray::Add<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)
120
121
0
  void Write(std::ostream& o, uint32_t indent) const override {
122
0
    std::string indent_str(indent, ' ');
123
0
    o << "[";
124
0
    bool is_first = true;
125
0
    for (const auto& value : values_) {
126
0
      if (!is_first) {
127
0
        o << ",";
128
0
      }
129
0
      is_first = false;
130
0
      o << "\n" << indent_str << "  ";
131
0
      value->Write(o, indent + 2);
132
0
    }
133
0
    if (!values_.empty()) {
134
0
      o << "\n" << indent_str;
135
0
    }
136
0
    o << "]";
137
0
  }
138
139
 private:
140
  std::vector<std::unique_ptr<JSONField>> values_;
141
};
142
143
0
void GenerateMetadata(const PackedPixelFile& ppf, std::vector<uint8_t>* out) {
144
0
  JSONDict meta;
145
  // Same order as in 18181-3 CD.
146
147
  // Frames.
148
0
  auto* meta_frames = meta.AddEmpty<JSONArray>("frames");
149
0
  for (size_t i = 0; i < ppf.frames.size(); i++) {
150
0
    auto* frame_i = meta_frames->AddEmpty<JSONDict>();
151
0
    if (ppf.info.have_animation) {
152
0
      frame_i->Add("duration",
153
0
                   JSONValue(ppf.frames[i].frame_info.duration * 1.0f *
154
0
                             ppf.info.animation.tps_denominator /
155
0
                             ppf.info.animation.tps_numerator));
156
0
    }
157
158
0
    frame_i->Add("name", JSONValue(ppf.frames[i].name));
159
160
0
    if (ppf.info.animation.have_timecodes) {
161
0
      frame_i->Add("timecode", JSONValue(ppf.frames[i].frame_info.timecode));
162
0
    }
163
0
  }
164
165
0
#define METADATA(FIELD) meta.Add(#FIELD, ppf.info.FIELD)
166
167
0
  METADATA(intensity_target);
168
0
  METADATA(min_nits);
169
0
  METADATA(relative_to_max_display);
170
0
  METADATA(linear_below);
171
172
0
  if (ppf.info.have_preview) {
173
0
    meta.AddEmpty<JSONDict>("preview");
174
    // TODO(veluca): can we have duration/name/timecode here?
175
0
  }
176
177
0
  {
178
0
    auto* ectype = meta.AddEmpty<JSONArray>("extra_channel_type");
179
0
    auto* bps = meta.AddEmpty<JSONArray>("bits_per_sample");
180
0
    auto* ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample");
181
0
    bps->Add(ppf.info.bits_per_sample);
182
0
    ebps->Add(ppf.info.exponent_bits_per_sample);
183
0
    for (const auto& eci : ppf.extra_channels_info) {
184
0
      switch (eci.ec_info.type) {
185
0
        case JXL_CHANNEL_ALPHA: {
186
0
          ectype->Add(std::string("Alpha"));
187
0
          break;
188
0
        }
189
0
        case JXL_CHANNEL_DEPTH: {
190
0
          ectype->Add(std::string("Depth"));
191
0
          break;
192
0
        }
193
0
        case JXL_CHANNEL_SPOT_COLOR: {
194
0
          ectype->Add(std::string("SpotColor"));
195
0
          break;
196
0
        }
197
0
        case JXL_CHANNEL_SELECTION_MASK: {
198
0
          ectype->Add(std::string("SelectionMask"));
199
0
          break;
200
0
        }
201
0
        case JXL_CHANNEL_BLACK: {
202
0
          ectype->Add(std::string("Black"));
203
0
          break;
204
0
        }
205
0
        case JXL_CHANNEL_CFA: {
206
0
          ectype->Add(std::string("CFA"));
207
0
          break;
208
0
        }
209
0
        case JXL_CHANNEL_THERMAL: {
210
0
          ectype->Add(std::string("Thermal"));
211
0
          break;
212
0
        }
213
0
        default: {
214
0
          ectype->Add(std::string("UNKNOWN"));
215
0
          break;
216
0
        }
217
0
      }
218
0
      bps->Add(eci.ec_info.bits_per_sample);
219
0
      ebps->Add(eci.ec_info.exponent_bits_per_sample);
220
0
    }
221
0
  }
222
223
0
  std::ostringstream os;
224
0
  meta.Write(os, 0);
225
0
  out->resize(os.str().size());
226
0
  memcpy(out->data(), os.str().data(), os.str().size());
227
0
}
228
229
0
void Append(std::vector<uint8_t>* out, const void* data, size_t size) {
230
0
  size_t pos = out->size();
231
0
  out->resize(pos + size);
232
0
  memcpy(out->data() + pos, data, size);
233
0
}
234
235
void WriteNPYHeader(size_t xsize, size_t ysize, uint32_t num_channels,
236
0
                    size_t num_frames, std::vector<uint8_t>* out) {
237
0
  const uint8_t header[] = "\x93NUMPY\x01\x00";
238
0
  Append(out, header, 8);
239
0
  std::stringstream ss;
240
0
  ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" << num_frames
241
0
     << ", " << ysize << ", " << xsize << ", " << num_channels << "), }\n";
242
  // 16-bit little endian header length.
243
0
  uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256),
244
0
                           static_cast<uint8_t>(ss.str().size() / 256)};
245
0
  Append(out, header_len, 2);
246
0
  Append(out, ss.str().data(), ss.str().size());
247
0
}
248
249
bool WriteFrameToNPYArray(size_t xsize, size_t ysize, const PackedFrame& frame,
250
0
                          std::vector<uint8_t>* out) {
251
0
  const auto& color = frame.color;
252
0
  if (color.xsize != xsize || color.ysize != ysize) {
253
0
    return false;
254
0
  }
255
0
  for (const auto& ec : frame.extra_channels) {
256
0
    if (ec.xsize != xsize || ec.ysize != ysize) {
257
0
      return false;
258
0
    }
259
0
  }
260
  // interleave the samples from color and extra channels
261
0
  for (size_t y = 0; y < ysize; ++y) {
262
0
    for (size_t x = 0; x < xsize; ++x) {
263
0
      {
264
0
        size_t sample_size = color.pixel_stride();
265
0
        size_t offset = y * color.stride + x * sample_size;
266
0
        uint8_t* pixels = reinterpret_cast<uint8_t*>(color.pixels());
267
0
        JXL_ENSURE(offset + sample_size <= color.pixels_size);
268
0
        Append(out, pixels + offset, sample_size);
269
0
      }
270
0
      for (const auto& ec : frame.extra_channels) {
271
0
        size_t sample_size = ec.pixel_stride();
272
0
        size_t offset = y * ec.stride + x * sample_size;
273
0
        uint8_t* pixels = reinterpret_cast<uint8_t*>(ec.pixels());
274
0
        JXL_ENSURE(offset + sample_size <= ec.pixels_size);
275
0
        Append(out, pixels + offset, sample_size);
276
0
      }
277
0
    }
278
0
  }
279
0
  return true;
280
0
}
281
282
// Writes a PackedPixelFile as a numpy 4D ndarray in binary format.
283
0
bool WriteNPYArray(const PackedPixelFile& ppf, std::vector<uint8_t>* out) {
284
0
  size_t xsize = ppf.info.xsize;
285
0
  size_t ysize = ppf.info.ysize;
286
0
  WriteNPYHeader(xsize, ysize,
287
0
                 ppf.info.num_color_channels + ppf.extra_channels_info.size(),
288
0
                 ppf.frames.size(), out);
289
0
  for (const auto& frame : ppf.frames) {
290
0
    if (!WriteFrameToNPYArray(xsize, ysize, frame, out)) {
291
0
      return false;
292
0
    }
293
0
  }
294
0
  return true;
295
0
}
296
297
class NumPyEncoder : public Encoder {
298
 public:
299
  Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image,
300
0
                ThreadPool* pool) const override {
301
0
    JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info));
302
0
    GenerateMetadata(ppf, &encoded_image->metadata);
303
0
    encoded_image->bitstreams.emplace_back();
304
0
    if (!WriteNPYArray(ppf, &encoded_image->bitstreams.back())) {
305
0
      return false;
306
0
    }
307
0
    if (ppf.preview_frame) {
308
0
      size_t xsize = ppf.info.preview.xsize;
309
0
      size_t ysize = ppf.info.preview.ysize;
310
0
      WriteNPYHeader(xsize, ysize, ppf.info.num_color_channels, 1,
311
0
                     &encoded_image->preview_bitstream);
312
0
      if (!WriteFrameToNPYArray(xsize, ysize, *ppf.preview_frame,
313
0
                                &encoded_image->preview_bitstream)) {
314
0
        return false;
315
0
      }
316
0
    }
317
0
    return true;
318
0
  }
319
0
  std::vector<JxlPixelFormat> AcceptedFormats() const override {
320
0
    std::vector<JxlPixelFormat> formats;
321
0
    for (const uint32_t num_channels : {1, 3}) {
322
0
      formats.push_back(JxlPixelFormat{num_channels, JXL_TYPE_FLOAT,
323
0
                                       JXL_LITTLE_ENDIAN, /*align=*/0});
324
0
    }
325
0
    return formats;
326
0
  }
327
0
  bool AcceptsCmyk() const override { return true; }
328
};
329
330
}  // namespace
331
332
0
std::unique_ptr<Encoder> GetNumPyEncoder() {
333
0
  return jxl::make_unique<NumPyEncoder>();
334
0
}
335
336
}  // namespace extras
337
}  // namespace jxl