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/jxl/splines.h"
7
8 #include <algorithm>
9
10 #include "lib/jxl/ans_params.h"
11 #include "lib/jxl/base/status.h"
12 #include "lib/jxl/chroma_from_luma.h"
13 #include "lib/jxl/common.h"
14 #include "lib/jxl/dct_scales.h"
15 #include "lib/jxl/entropy_coder.h"
16 #include "lib/jxl/opsin_params.h"
17
18 #undef HWY_TARGET_INCLUDE
19 #define HWY_TARGET_INCLUDE "lib/jxl/splines.cc"
20 #include <hwy/foreach_target.h>
21 #include <hwy/highway.h>
22
23 #include "lib/jxl/fast_math-inl.h"
24 HWY_BEFORE_NAMESPACE();
25 namespace jxl {
26 namespace HWY_NAMESPACE {
27 namespace {
28
29 // Given a set of DCT coefficients, this returns the result of performing cosine
30 // interpolation on the original samples.
ContinuousIDCT(const float dct[32],float t)31 float ContinuousIDCT(const float dct[32], float t) {
32 // We compute here the DCT-3 of the `dct` vector, rescaled by a factor of
33 // sqrt(32). This is such that an input vector vector {x, 0, ..., 0} produces
34 // a constant result of x. dct[0] was scaled in Dequantize() to allow uniform
35 // treatment of all the coefficients.
36 constexpr float kMultipliers[32] = {
37 kPi / 32 * 0, kPi / 32 * 1, kPi / 32 * 2, kPi / 32 * 3, kPi / 32 * 4,
38 kPi / 32 * 5, kPi / 32 * 6, kPi / 32 * 7, kPi / 32 * 8, kPi / 32 * 9,
39 kPi / 32 * 10, kPi / 32 * 11, kPi / 32 * 12, kPi / 32 * 13, kPi / 32 * 14,
40 kPi / 32 * 15, kPi / 32 * 16, kPi / 32 * 17, kPi / 32 * 18, kPi / 32 * 19,
41 kPi / 32 * 20, kPi / 32 * 21, kPi / 32 * 22, kPi / 32 * 23, kPi / 32 * 24,
42 kPi / 32 * 25, kPi / 32 * 26, kPi / 32 * 27, kPi / 32 * 28, kPi / 32 * 29,
43 kPi / 32 * 30, kPi / 32 * 31,
44 };
45 HWY_CAPPED(float, 32) df;
46 auto result = Zero(df);
47 const auto tandhalf = Set(df, t + 0.5f);
48 for (int i = 0; i < 32; i += Lanes(df)) {
49 auto cos_arg = LoadU(df, kMultipliers + i) * tandhalf;
50 auto cos = FastCosf(df, cos_arg);
51 auto local_res = LoadU(df, dct + i) * cos;
52 result = MulAdd(Set(df, square_root<2>::value), local_res, result);
53 }
54 return GetLane(SumOfLanes(result));
55 }
56
57 // Splats a single Gaussian on the image.
DrawGaussian(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const Spline::Point & center,const float intensity,const float color[3],const float sigma,std::vector<int32_t> & xs,std::vector<int32_t> & ys,std::vector<float> & local_intensity_storage)58 void DrawGaussian(Image3F* const opsin, const Rect& opsin_rect,
59 const Rect& image_rect, const Spline::Point& center,
60 const float intensity, const float color[3],
61 const float sigma, std::vector<int32_t>& xs,
62 std::vector<int32_t>& ys,
63 std::vector<float>& local_intensity_storage) {
64 constexpr float kDistanceMultiplier = 4.605170185988091f; // -2 * log(0.1)
65 // Distance beyond which exp(-d^2 / (2 * sigma^2)) drops below 0.1.
66 const float maximum_distance = sigma * sigma * kDistanceMultiplier;
67 const auto xbegin_s =
68 std::max<ssize_t>(image_rect.x0(), center.x - maximum_distance + .5f);
69 const auto xend_s =
70 std::min<ssize_t>(center.x + maximum_distance + .5f,
71 image_rect.x0() + image_rect.xsize() - 1);
72 const auto ybegin_s =
73 std::max<ssize_t>(image_rect.y0(), center.y - maximum_distance + .5f);
74 const auto yend_s =
75 std::min<ssize_t>(center.y + maximum_distance + .5f,
76 image_rect.y0() + image_rect.ysize() - 1);
77 if ((xend_s) <= 0 || (xend_s < xbegin_s)) return;
78 const size_t xbegin = xbegin_s;
79 const size_t xend = xend_s;
80 if ((yend_s <= 0) || (yend_s < ybegin_s)) return;
81 const size_t ybegin = ybegin_s;
82 const size_t yend = yend_s;
83 const size_t opsin_stride = opsin->PixelsPerRow();
84 float* JXL_RESTRICT rows[3] = {
85 opsin_rect.PlaneRow(opsin, 0, ybegin - image_rect.y0()),
86 opsin_rect.PlaneRow(opsin, 1, ybegin - image_rect.y0()),
87 opsin_rect.PlaneRow(opsin, 2, ybegin - image_rect.y0()),
88 };
89 const size_t nx = xend + 1 - xbegin;
90 const size_t ny = yend + 1 - ybegin;
91 HWY_FULL(float) df;
92 if (xs.size() < nx * ny) {
93 size_t sz = DivCeil(nx * ny, Lanes(df)) * Lanes(df);
94 xs.resize(sz);
95 ys.resize(sz);
96 local_intensity_storage.resize(sz);
97 }
98 for (size_t y = ybegin; y <= yend; ++y) {
99 for (size_t x = xbegin; x <= xend; ++x) {
100 xs[(y - ybegin) * nx + (x - xbegin)] = x;
101 ys[(y - ybegin) * nx + (x - xbegin)] = y;
102 }
103 }
104 Rebind<int32_t, decltype(df)> di;
105 const auto inv_sigma = Set(df, 1.0f / sigma);
106 const auto half = Set(df, 0.5f);
107 const auto one_over_2s2 = Set(df, 0.353553391f);
108 const auto sigma_over_4_times_intensity = Set(df, .25f * sigma * intensity);
109 for (size_t i = 0; i < nx * ny; i += Lanes(df)) {
110 const auto x = ConvertTo(df, LoadU(di, &xs[i]));
111 const auto y = ConvertTo(df, LoadU(di, &ys[i]));
112 const auto dx = x - Set(df, center.x);
113 const auto dy = y - Set(df, center.y);
114 const auto sqd = MulAdd(dx, dx, dy * dy);
115 const auto distance = Sqrt(sqd);
116 const auto one_dimensional_factor =
117 FastErff(df, MulAdd(distance, half, one_over_2s2) * inv_sigma) -
118 FastErff(df, MulSub(distance, half, one_over_2s2) * inv_sigma);
119 const auto local_intensity = sigma_over_4_times_intensity *
120 one_dimensional_factor *
121 one_dimensional_factor;
122 StoreU(local_intensity, df, &local_intensity_storage[i]);
123 }
124 ssize_t off = -static_cast<ssize_t>(image_rect.x0());
125 for (size_t y = ybegin; y <= yend; ++y) {
126 HWY_CAPPED(float, 1) df;
127 for (size_t x = xbegin; x <= xend; ++x) {
128 const auto local_intensity = Load(
129 df, local_intensity_storage.data() + (y - ybegin) * nx + x - xbegin);
130 for (size_t c = 0; c < 3; ++c) {
131 const auto cm = Set(df, color[c]);
132 const auto in = LoadU(df, rows[c] + x + off);
133 StoreU(MulAdd(cm, local_intensity, in), df, rows[c] + x + off);
134 }
135 }
136 off += opsin_stride;
137 }
138 }
139
DrawFromPoints(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const Spline & spline,bool add,const std::vector<std::pair<Spline::Point,float>> & points_to_draw,float arc_length)140 void DrawFromPoints(
141 Image3F* const opsin, const Rect& opsin_rect, const Rect& image_rect,
142 const Spline& spline, bool add,
143 const std::vector<std::pair<Spline::Point, float>>& points_to_draw,
144 float arc_length) {
145 float inv_arc_length = 1.0f / arc_length;
146 int k = 0;
147 std::vector<int32_t> xs, ys;
148 std::vector<float> local_intensity_storage;
149 for (const auto& point_to_draw : points_to_draw) {
150 const Spline::Point& point = point_to_draw.first;
151 const float multiplier = add ? point_to_draw.second : -point_to_draw.second;
152 const float progress_along_arc =
153 std::min(1.f, (k * kDesiredRenderingDistance) * inv_arc_length);
154 ++k;
155 float color[3];
156 for (size_t c = 0; c < 3; ++c) {
157 color[c] =
158 ContinuousIDCT(spline.color_dct[c], (32 - 1) * progress_along_arc);
159 }
160 const float sigma =
161 ContinuousIDCT(spline.sigma_dct, (32 - 1) * progress_along_arc);
162 DrawGaussian(opsin, opsin_rect, image_rect, point, multiplier, color, sigma,
163 xs, ys, local_intensity_storage);
164 }
165 }
166 } // namespace
167 // NOLINTNEXTLINE(google-readability-namespace-comments)
168 } // namespace HWY_NAMESPACE
169 } // namespace jxl
170 HWY_AFTER_NAMESPACE();
171
172 #if HWY_ONCE
173 namespace jxl {
174 HWY_EXPORT(DrawFromPoints);
175
176 namespace {
177
178 // Maximum number of spline control points per frame is
179 // std::min(kMaxNumControlPoints, xsize * ysize / 2)
180 constexpr size_t kMaxNumControlPoints = 1u << 20u;
181 constexpr size_t kMaxNumControlPointsPerPixelRatio = 2;
182
183 // X, Y, B, sigma.
ColorQuantizationWeight(const int32_t adjustment,const int channel,const int i)184 float ColorQuantizationWeight(const int32_t adjustment, const int channel,
185 const int i) {
186 const float multiplier = adjustment >= 0 ? 1.f + .125f * adjustment
187 : 1.f / (1.f + .125f * -adjustment);
188
189 static constexpr float kChannelWeight[] = {0.0042f, 0.075f, 0.07f, .3333f};
190
191 return multiplier / kChannelWeight[channel];
192 }
193
DecodeAllStartingPoints(std::vector<Spline::Point> * const points,BitReader * const br,ANSSymbolReader * reader,const std::vector<uint8_t> & context_map,size_t num_splines)194 Status DecodeAllStartingPoints(std::vector<Spline::Point>* const points,
195 BitReader* const br, ANSSymbolReader* reader,
196 const std::vector<uint8_t>& context_map,
197 size_t num_splines) {
198 points->clear();
199 points->reserve(num_splines);
200 int64_t last_x = 0;
201 int64_t last_y = 0;
202 for (size_t i = 0; i < num_splines; i++) {
203 int64_t x =
204 reader->ReadHybridUint(kStartingPositionContext, br, context_map);
205 int64_t y =
206 reader->ReadHybridUint(kStartingPositionContext, br, context_map);
207 if (i != 0) {
208 x = UnpackSigned(x) + last_x;
209 y = UnpackSigned(y) + last_y;
210 }
211 points->emplace_back(static_cast<float>(x), static_cast<float>(y));
212 last_x = x;
213 last_y = y;
214 }
215 return true;
216 }
217
218 struct Vector {
219 float x, y;
operator -jxl::__anon1da3897f0211::Vector220 Vector operator-() const { return {-x, -y}; }
operator +jxl::__anon1da3897f0211::Vector221 Vector operator+(const Vector& other) const {
222 return {x + other.x, y + other.y};
223 }
SquaredNormjxl::__anon1da3897f0211::Vector224 float SquaredNorm() const { return x * x + y * y; }
225 };
operator *(const float k,const Vector & vec)226 Vector operator*(const float k, const Vector& vec) {
227 return {k * vec.x, k * vec.y};
228 }
229
operator +(const Spline::Point & p,const Vector & vec)230 Spline::Point operator+(const Spline::Point& p, const Vector& vec) {
231 return {p.x + vec.x, p.y + vec.y};
232 }
operator -(const Spline::Point & p,const Vector & vec)233 Spline::Point operator-(const Spline::Point& p, const Vector& vec) {
234 return p + -vec;
235 }
operator -(const Spline::Point & a,const Spline::Point & b)236 Vector operator-(const Spline::Point& a, const Spline::Point& b) {
237 return {a.x - b.x, a.y - b.y};
238 }
239
DrawCentripetalCatmullRomSpline(std::vector<Spline::Point> points)240 std::vector<Spline::Point> DrawCentripetalCatmullRomSpline(
241 std::vector<Spline::Point> points) {
242 if (points.size() <= 1) return points;
243 // Number of points to compute between each control point.
244 static constexpr int kNumPoints = 16;
245 std::vector<Spline::Point> result;
246 result.reserve((points.size() - 1) * kNumPoints + 1);
247 points.insert(points.begin(), points[0] + (points[0] - points[1]));
248 points.push_back(points[points.size() - 1] +
249 (points[points.size() - 1] - points[points.size() - 2]));
250 // points has at least 4 elements at this point.
251 for (size_t start = 0; start < points.size() - 3; ++start) {
252 // 4 of them are used, and we draw from p[1] to p[2].
253 const Spline::Point* const p = &points[start];
254 result.push_back(p[1]);
255 float t[4] = {0};
256 for (int k = 1; k < 4; ++k) {
257 t[k] = std::sqrt(hypotf(p[k].x - p[k - 1].x, p[k].y - p[k - 1].y)) +
258 t[k - 1];
259 }
260 for (int i = 1; i < kNumPoints; ++i) {
261 const float tt =
262 t[1] + (static_cast<float>(i) / kNumPoints) * (t[2] - t[1]);
263 Spline::Point a[3];
264 for (int k = 0; k < 3; ++k) {
265 a[k] = p[k] + ((tt - t[k]) / (t[k + 1] - t[k])) * (p[k + 1] - p[k]);
266 }
267 Spline::Point b[2];
268 for (int k = 0; k < 2; ++k) {
269 b[k] = a[k] + ((tt - t[k]) / (t[k + 2] - t[k])) * (a[k + 1] - a[k]);
270 }
271 result.push_back(b[0] + ((tt - t[1]) / (t[2] - t[1])) * (b[1] - b[0]));
272 }
273 }
274 result.push_back(points[points.size() - 2]);
275 return result;
276 }
277
278 // Move along the line segments defined by `points`, `kDesiredRenderingDistance`
279 // pixels at a time, and call `functor` with each point and the actual distance
280 // to the previous point (which will always be kDesiredRenderingDistance except
281 // possibly for the very last point).
282 template <typename Points, typename Functor>
ForEachEquallySpacedPoint(const Points & points,const Functor & functor)283 void ForEachEquallySpacedPoint(const Points& points, const Functor& functor) {
284 JXL_ASSERT(!points.empty());
285 Spline::Point current = points.front();
286 functor(current, kDesiredRenderingDistance);
287 auto next = points.begin();
288 while (next != points.end()) {
289 const Spline::Point* previous = ¤t;
290 float arclength_from_previous = 0.f;
291 for (;;) {
292 if (next == points.end()) {
293 functor(*previous, arclength_from_previous);
294 return;
295 }
296 const float arclength_to_next =
297 std::sqrt((*next - *previous).SquaredNorm());
298 if (arclength_from_previous + arclength_to_next >=
299 kDesiredRenderingDistance) {
300 current =
301 *previous + ((kDesiredRenderingDistance - arclength_from_previous) /
302 arclength_to_next) *
303 (*next - *previous);
304 functor(current, kDesiredRenderingDistance);
305 break;
306 }
307 arclength_from_previous += arclength_to_next;
308 previous = &*next;
309 ++next;
310 }
311 }
312 }
313
314 } // namespace
315
QuantizedSpline(const Spline & original,const int32_t quantization_adjustment,float ytox,float ytob)316 QuantizedSpline::QuantizedSpline(const Spline& original,
317 const int32_t quantization_adjustment,
318 float ytox, float ytob) {
319 JXL_ASSERT(!original.control_points.empty());
320 control_points_.reserve(original.control_points.size() - 1);
321 const Spline::Point& starting_point = original.control_points.front();
322 int previous_x = static_cast<int>(roundf(starting_point.x)),
323 previous_y = static_cast<int>(roundf(starting_point.y));
324 int previous_delta_x = 0, previous_delta_y = 0;
325 for (auto it = original.control_points.begin() + 1;
326 it != original.control_points.end(); ++it) {
327 const int new_x = static_cast<int>(roundf(it->x));
328 const int new_y = static_cast<int>(roundf(it->y));
329 const int new_delta_x = new_x - previous_x;
330 const int new_delta_y = new_y - previous_y;
331 control_points_.emplace_back(new_delta_x - previous_delta_x,
332 new_delta_y - previous_delta_y);
333 previous_delta_x = new_delta_x;
334 previous_delta_y = new_delta_y;
335 previous_x = new_x;
336 previous_y = new_y;
337 }
338
339 for (int c = 0; c < 3; ++c) {
340 float factor = c == 0 ? ytox : c == 1 ? 0 : ytob;
341 for (int i = 0; i < 32; ++i) {
342 const float coefficient =
343 original.color_dct[c][i] -
344 factor * color_dct_[1][i] /
345 ColorQuantizationWeight(quantization_adjustment, 1, i);
346 color_dct_[c][i] = static_cast<int>(
347 roundf(coefficient *
348 ColorQuantizationWeight(quantization_adjustment, c, i)));
349 }
350 }
351 for (int i = 0; i < 32; ++i) {
352 sigma_dct_[i] = static_cast<int>(
353 roundf(original.sigma_dct[i] *
354 ColorQuantizationWeight(quantization_adjustment, 3, i)));
355 }
356 }
357
Dequantize(const Spline::Point & starting_point,const int32_t quantization_adjustment,float ytox,float ytob) const358 Spline QuantizedSpline::Dequantize(const Spline::Point& starting_point,
359 const int32_t quantization_adjustment,
360 float ytox, float ytob) const {
361 Spline result;
362
363 result.control_points.reserve(control_points_.size() + 1);
364 int current_x = static_cast<int>(roundf(starting_point.x)),
365 current_y = static_cast<int>(roundf(starting_point.y));
366 result.control_points.push_back(Spline::Point{static_cast<float>(current_x),
367 static_cast<float>(current_y)});
368 int current_delta_x = 0, current_delta_y = 0;
369 for (const auto& point : control_points_) {
370 current_delta_x += point.first;
371 current_delta_y += point.second;
372 current_x += current_delta_x;
373 current_y += current_delta_y;
374 result.control_points.push_back(Spline::Point{
375 static_cast<float>(current_x), static_cast<float>(current_y)});
376 }
377
378 for (int c = 0; c < 3; ++c) {
379 for (int i = 0; i < 32; ++i) {
380 result.color_dct[c][i] =
381 color_dct_[c][i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) /
382 ColorQuantizationWeight(quantization_adjustment, c, i);
383 }
384 }
385 for (int i = 0; i < 32; ++i) {
386 result.color_dct[0][i] += ytox * result.color_dct[1][i];
387 result.color_dct[2][i] += ytob * result.color_dct[1][i];
388 }
389 for (int i = 0; i < 32; ++i) {
390 result.sigma_dct[i] =
391 sigma_dct_[i] * (i == 0 ? 1.0f / square_root<2>::value : 1.0f) /
392 ColorQuantizationWeight(quantization_adjustment, 3, i);
393 }
394
395 return result;
396 }
397
Decode(const std::vector<uint8_t> & context_map,ANSSymbolReader * const decoder,BitReader * const br,size_t max_control_points,size_t * total_num_control_points)398 Status QuantizedSpline::Decode(const std::vector<uint8_t>& context_map,
399 ANSSymbolReader* const decoder,
400 BitReader* const br, size_t max_control_points,
401 size_t* total_num_control_points) {
402 const size_t num_control_points =
403 decoder->ReadHybridUint(kNumControlPointsContext, br, context_map);
404 *total_num_control_points += num_control_points;
405 if (*total_num_control_points > max_control_points) {
406 return JXL_FAILURE("Too many control points: %zu",
407 *total_num_control_points);
408 }
409 control_points_.resize(num_control_points);
410 for (std::pair<int64_t, int64_t>& control_point : control_points_) {
411 control_point.first = UnpackSigned(
412 decoder->ReadHybridUint(kControlPointsContext, br, context_map));
413 control_point.second = UnpackSigned(
414 decoder->ReadHybridUint(kControlPointsContext, br, context_map));
415 }
416
417 const auto decode_dct = [decoder, br, &context_map](int dct[32]) -> Status {
418 for (int i = 0; i < 32; ++i) {
419 dct[i] =
420 UnpackSigned(decoder->ReadHybridUint(kDCTContext, br, context_map));
421 }
422 return true;
423 };
424 for (int c = 0; c < 3; ++c) {
425 JXL_RETURN_IF_ERROR(decode_dct(color_dct_[c]));
426 }
427 JXL_RETURN_IF_ERROR(decode_dct(sigma_dct_));
428 return true;
429 }
430
Decode(jxl::BitReader * br,size_t num_pixels)431 Status Splines::Decode(jxl::BitReader* br, size_t num_pixels) {
432 std::vector<uint8_t> context_map;
433 ANSCode code;
434 JXL_RETURN_IF_ERROR(
435 DecodeHistograms(br, kNumSplineContexts, &code, &context_map));
436 ANSSymbolReader decoder(&code, br);
437 const size_t num_splines =
438 1 + decoder.ReadHybridUint(kNumSplinesContext, br, context_map);
439 size_t max_control_points = std::min(
440 kMaxNumControlPoints, num_pixels / kMaxNumControlPointsPerPixelRatio);
441 if (num_splines > max_control_points) {
442 return JXL_FAILURE("Too many splines: %zu", num_splines);
443 }
444 JXL_RETURN_IF_ERROR(DecodeAllStartingPoints(&starting_points_, br, &decoder,
445 context_map, num_splines));
446
447 quantization_adjustment_ = UnpackSigned(
448 decoder.ReadHybridUint(kQuantizationAdjustmentContext, br, context_map));
449
450 splines_.clear();
451 splines_.reserve(num_splines);
452 size_t num_control_points = num_splines;
453 for (size_t i = 0; i < num_splines; ++i) {
454 QuantizedSpline spline;
455 JXL_RETURN_IF_ERROR(spline.Decode(context_map, &decoder, br,
456 max_control_points, &num_control_points));
457 splines_.push_back(std::move(spline));
458 }
459
460 JXL_RETURN_IF_ERROR(decoder.CheckANSFinalState());
461
462 if (!HasAny()) {
463 return JXL_FAILURE("Decoded splines but got none");
464 }
465
466 return true;
467 }
468
AddTo(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const ColorCorrelationMap & cmap) const469 Status Splines::AddTo(Image3F* const opsin, const Rect& opsin_rect,
470 const Rect& image_rect,
471 const ColorCorrelationMap& cmap) const {
472 return Apply</*add=*/true>(opsin, opsin_rect, image_rect, cmap);
473 }
474
SubtractFrom(Image3F * const opsin,const ColorCorrelationMap & cmap) const475 Status Splines::SubtractFrom(Image3F* const opsin,
476 const ColorCorrelationMap& cmap) const {
477 return Apply</*add=*/false>(opsin, Rect(*opsin), Rect(*opsin), cmap);
478 }
479
480 template <bool add>
Apply(Image3F * const opsin,const Rect & opsin_rect,const Rect & image_rect,const ColorCorrelationMap & cmap) const481 Status Splines::Apply(Image3F* const opsin, const Rect& opsin_rect,
482 const Rect& image_rect,
483 const ColorCorrelationMap& cmap) const {
484 for (size_t i = 0; i < splines_.size(); ++i) {
485 const Spline spline =
486 splines_[i].Dequantize(starting_points_[i], quantization_adjustment_,
487 cmap.YtoXRatio(0), cmap.YtoBRatio(0));
488 if (std::adjacent_find(spline.control_points.begin(),
489 spline.control_points.end()) !=
490 spline.control_points.end()) {
491 return JXL_FAILURE("identical successive control points in spline %zu",
492 i);
493 }
494 std::vector<std::pair<Spline::Point, float>> points_to_draw;
495 ForEachEquallySpacedPoint(
496 DrawCentripetalCatmullRomSpline(spline.control_points),
497 [&](const Spline::Point& point, const float multiplier) {
498 points_to_draw.emplace_back(point, multiplier);
499 });
500 const float arc_length =
501 (points_to_draw.size() - 2) * kDesiredRenderingDistance +
502 points_to_draw.back().second;
503 if (arc_length <= 0.f) {
504 // This spline wouldn't have any effect.
505 continue;
506 }
507 HWY_DYNAMIC_DISPATCH(DrawFromPoints)
508 (opsin, opsin_rect, image_rect, spline, add, points_to_draw, arc_length);
509 }
510 return true;
511 }
512
513 } // namespace jxl
514 #endif // HWY_ONCE
515