Coverage Report

Created: 2026-02-14 06:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mupdf/source/xps/xps-gradient.c
Line
Count
Source
1
// Copyright (C) 2004-2021 Artifex Software, Inc.
2
//
3
// This file is part of MuPDF.
4
//
5
// MuPDF is free software: you can redistribute it and/or modify it under the
6
// terms of the GNU Affero General Public License as published by the Free
7
// Software Foundation, either version 3 of the License, or (at your option)
8
// any later version.
9
//
10
// MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
11
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13
// details.
14
//
15
// You should have received a copy of the GNU Affero General Public License
16
// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
17
//
18
// Alternative licensing terms are available from the licensor.
19
// For commercial licensing, see <https://www.artifex.com/> or contact
20
// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
21
// CA 94129, USA, for further information.
22
23
#include "mupdf/fitz.h"
24
#include "xps-imp.h"
25
26
#include <string.h>
27
#include <math.h>
28
#include <float.h>
29
#include <stdlib.h>
30
31
0
#define MAX_STOPS 256
32
33
enum { SPREAD_PAD, SPREAD_REPEAT, SPREAD_REFLECT };
34
35
/*
36
 * Parse a list of GradientStop elements.
37
 * Fill the offset and color arrays, and
38
 * return the number of stops parsed.
39
 */
40
41
struct stop
42
{
43
  float offset;
44
  float r, g, b, a;
45
  int index;
46
};
47
48
static int cmp_stop(const void *a, const void *b)
49
0
{
50
0
  const struct stop *astop = a;
51
0
  const struct stop *bstop = b;
52
0
  float diff = astop->offset - bstop->offset;
53
0
  if (diff < 0)
54
0
    return -1;
55
0
  if (diff > 0)
56
0
    return 1;
57
0
  return astop->index - bstop->index;
58
0
}
59
60
static inline float lerp(float a, float b, float x)
61
0
{
62
0
  return a + (b - a) * x;
63
0
}
64
65
static int
66
xps_parse_gradient_stops(fz_context *ctx, xps_document *doc, char *base_uri, fz_xml *node,
67
  struct stop *stops, int maxcount)
68
0
{
69
0
  fz_colorspace *colorspace;
70
0
  float sample[FZ_MAX_COLORS];
71
0
  float rgb[3];
72
0
  int before, after;
73
0
  int count;
74
0
  int i;
75
76
  /* We may have to insert 2 extra stops when postprocessing */
77
0
  maxcount -= 2;
78
79
0
  count = 0;
80
0
  while (node && count < maxcount)
81
0
  {
82
0
    if (fz_xml_is_tag(node, "GradientStop"))
83
0
    {
84
0
      char *offset = fz_xml_att(node, "Offset");
85
0
      char *color = fz_xml_att(node, "Color");
86
0
      if (offset && color)
87
0
      {
88
0
        stops[count].offset = fz_atof(offset);
89
0
        stops[count].index = count;
90
91
0
        xps_parse_color(ctx, doc, base_uri, color, &colorspace, sample);
92
93
0
        fz_convert_color(ctx, colorspace, sample+1, fz_device_rgb(ctx), rgb, NULL, fz_default_color_params);
94
95
0
        stops[count].r = rgb[0];
96
0
        stops[count].g = rgb[1];
97
0
        stops[count].b = rgb[2];
98
0
        stops[count].a = sample[0];
99
100
0
        count ++;
101
0
      }
102
0
    }
103
0
    node = fz_xml_next(node);
104
0
  }
105
106
0
  if (count == 0)
107
0
  {
108
0
    fz_warn(ctx, "gradient brush has no gradient stops");
109
0
    stops[0].offset = 0;
110
0
    stops[0].r = 0;
111
0
    stops[0].g = 0;
112
0
    stops[0].b = 0;
113
0
    stops[0].a = 1;
114
0
    stops[1].offset = 1;
115
0
    stops[1].r = 1;
116
0
    stops[1].g = 1;
117
0
    stops[1].b = 1;
118
0
    stops[1].a = 1;
119
0
    return 2;
120
0
  }
121
122
0
  if (count == maxcount)
123
0
    fz_warn(ctx, "gradient brush exceeded maximum number of gradient stops");
124
125
  /* Postprocess to make sure the range of offsets is 0.0 to 1.0 */
126
127
0
  qsort(stops, count, sizeof(struct stop), cmp_stop);
128
129
0
  before = -1;
130
0
  after = -1;
131
132
0
  for (i = 0; i < count; i++)
133
0
  {
134
0
    if (stops[i].offset < 0)
135
0
      before = i;
136
0
    if (stops[i].offset > 1)
137
0
    {
138
0
      after = i;
139
0
      break;
140
0
    }
141
0
  }
142
143
  /* Remove all stops < 0 except the largest one */
144
0
  if (before > 0)
145
0
  {
146
0
    memmove(stops, stops + before, (count - before) * sizeof(struct stop));
147
0
    count -= before;
148
0
  }
149
150
  /* Remove all stops > 1 except the smallest one */
151
0
  if (after >= 0)
152
0
    count = after + 1;
153
154
  /* Expand single stop to 0 .. 1 */
155
0
  if (count == 1)
156
0
  {
157
0
    stops[1] = stops[0];
158
0
    stops[0].offset = 0;
159
0
    stops[1].offset = 1;
160
0
    return 2;
161
0
  }
162
163
  /* First stop < 0 -- interpolate value to 0 */
164
0
  if (stops[0].offset < 0)
165
0
  {
166
0
    float d = -stops[0].offset / (stops[1].offset - stops[0].offset);
167
0
    stops[0].offset = 0;
168
0
    stops[0].r = lerp(stops[0].r, stops[1].r, d);
169
0
    stops[0].g = lerp(stops[0].g, stops[1].g, d);
170
0
    stops[0].b = lerp(stops[0].b, stops[1].b, d);
171
0
    stops[0].a = lerp(stops[0].a, stops[1].a, d);
172
0
  }
173
174
  /* Last stop > 1 -- interpolate value to 1 */
175
0
  if (stops[count-1].offset > 1)
176
0
  {
177
0
    float d = (1 - stops[count-2].offset) / (stops[count-1].offset - stops[count-2].offset);
178
0
    stops[count-1].offset = 1;
179
0
    stops[count-1].r = lerp(stops[count-2].r, stops[count-1].r, d);
180
0
    stops[count-1].g = lerp(stops[count-2].g, stops[count-1].g, d);
181
0
    stops[count-1].b = lerp(stops[count-2].b, stops[count-1].b, d);
182
0
    stops[count-1].a = lerp(stops[count-2].a, stops[count-1].a, d);
183
0
  }
184
185
  /* First stop > 0 -- insert a duplicate at 0 */
186
0
  if (stops[0].offset > 0)
187
0
  {
188
0
    memmove(stops + 1, stops, count * sizeof(struct stop));
189
0
    stops[0] = stops[1];
190
0
    stops[0].offset = 0;
191
0
    count++;
192
0
  }
193
194
  /* Last stop < 1 -- insert a duplicate at 1 */
195
0
  if (stops[count-1].offset < 1)
196
0
  {
197
0
    stops[count] = stops[count-1];
198
0
    stops[count].offset = 1;
199
0
    count++;
200
0
  }
201
202
0
  return count;
203
0
}
204
205
static void
206
xps_sample_gradient_stops(fz_context *ctx, xps_document *doc, fz_shade *shade, struct stop *stops, int count)
207
0
{
208
0
  float offset, d;
209
0
  int i, k;
210
211
0
  shade->function = fz_malloc(ctx, sizeof(float) * 256 * 4);
212
213
0
  k = 0;
214
0
  for (i = 0; i < 256; i++)
215
0
  {
216
0
    offset = i / 255.0f;
217
0
    while (k + 1 < count && offset > stops[k+1].offset)
218
0
      k++;
219
220
0
    d = (offset - stops[k].offset) / (stops[k+1].offset - stops[k].offset);
221
222
0
    shade->function[4*i + 0] = lerp(stops[k].r, stops[k+1].r, d);
223
0
    shade->function[4*i + 1] = lerp(stops[k].g, stops[k+1].g, d);
224
0
    shade->function[4*i + 2] = lerp(stops[k].b, stops[k+1].b, d);
225
0
    shade->function[4*i + 3] = lerp(stops[k].a, stops[k+1].a, d);
226
0
  }
227
0
}
228
229
/*
230
 * Radial gradients map more or less to Radial shadings.
231
 * The inner circle is always a point.
232
 * The outer circle is actually an ellipse,
233
 * mess with the transform to squash the circle into the right aspect.
234
 */
235
236
static void
237
xps_draw_one_radial_gradient(fz_context *ctx, xps_document *doc, fz_matrix ctm,
238
  struct stop *stops, int count,
239
  int extend,
240
  float x0, float y0, float r0,
241
  float x1, float y1, float r1)
242
0
{
243
0
  fz_device *dev = doc->dev;
244
0
  fz_shade *shade;
245
246
0
  shade = fz_malloc_struct(ctx, fz_shade);
247
0
  FZ_INIT_STORABLE(shade, 1, fz_drop_shade_imp);
248
0
  shade->colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
249
0
  shade->bbox = fz_infinite_rect;
250
0
  shade->matrix = fz_identity;
251
0
  shade->use_background = 0;
252
0
  shade->function_stride = 4;
253
0
  shade->type = FZ_RADIAL;
254
0
  shade->u.l_or_r.extend[0] = extend;
255
0
  shade->u.l_or_r.extend[1] = extend;
256
257
0
  shade->u.l_or_r.coords[0][0] = x0;
258
0
  shade->u.l_or_r.coords[0][1] = y0;
259
0
  shade->u.l_or_r.coords[0][2] = r0;
260
0
  shade->u.l_or_r.coords[1][0] = x1;
261
0
  shade->u.l_or_r.coords[1][1] = y1;
262
0
  shade->u.l_or_r.coords[1][2] = r1;
263
264
0
  fz_try(ctx)
265
0
  {
266
0
    xps_sample_gradient_stops(ctx, doc, shade, stops, count);
267
0
    fz_fill_shade(ctx, dev, shade, ctm, 1, fz_default_color_params);
268
0
  }
269
0
  fz_always(ctx)
270
0
    fz_drop_shade(ctx, shade);
271
0
  fz_catch(ctx)
272
0
    fz_rethrow(ctx);
273
0
}
274
275
/*
276
 * Linear gradients.
277
 */
278
279
static void
280
xps_draw_one_linear_gradient(fz_context *ctx, xps_document *doc, fz_matrix ctm,
281
  struct stop *stops, int count,
282
  int extend,
283
  float x0, float y0, float x1, float y1)
284
0
{
285
0
  fz_device *dev = doc->dev;
286
0
  fz_shade *shade;
287
288
0
  shade = fz_malloc_struct(ctx, fz_shade);
289
0
  FZ_INIT_STORABLE(shade, 1, fz_drop_shade_imp);
290
0
  shade->colorspace = fz_keep_colorspace(ctx, fz_device_rgb(ctx));
291
0
  shade->bbox = fz_infinite_rect;
292
0
  shade->matrix = fz_identity;
293
0
  shade->use_background = 0;
294
0
  shade->function_stride = 4;
295
0
  shade->type = FZ_LINEAR;
296
0
  shade->u.l_or_r.extend[0] = extend;
297
0
  shade->u.l_or_r.extend[1] = extend;
298
299
0
  shade->u.l_or_r.coords[0][0] = x0;
300
0
  shade->u.l_or_r.coords[0][1] = y0;
301
0
  shade->u.l_or_r.coords[0][2] = 0;
302
0
  shade->u.l_or_r.coords[1][0] = x1;
303
0
  shade->u.l_or_r.coords[1][1] = y1;
304
0
  shade->u.l_or_r.coords[1][2] = 0;
305
306
0
  fz_try(ctx)
307
0
  {
308
0
    xps_sample_gradient_stops(ctx, doc, shade, stops, count);
309
0
    fz_fill_shade(ctx, dev, shade, ctm, doc->opacity[doc->opacity_top], fz_default_color_params);
310
0
  }
311
0
  fz_always(ctx)
312
0
    fz_drop_shade(ctx, shade);
313
0
  fz_catch(ctx)
314
0
    fz_rethrow(ctx);
315
0
}
316
317
/*
318
 * We need to loop and create many shading objects to account
319
 * for the Repeat and Reflect SpreadMethods.
320
 * I'm not smart enough to calculate this analytically
321
 * so we iterate and check each object until we
322
 * reach a reasonable limit for infinite cases.
323
 */
324
325
static void
326
xps_draw_radial_gradient(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
327
  struct stop *stops, int count,
328
  fz_xml *root, int spread)
329
0
{
330
0
  float x0, y0, r0;
331
0
  float x1, y1, r1;
332
0
  float xrad = 1;
333
0
  float yrad = 1;
334
0
  float invscale;
335
0
  int i, ma = 1;
336
0
  fz_matrix inv;
337
338
0
  char *center_att = fz_xml_att(root, "Center");
339
0
  char *origin_att = fz_xml_att(root, "GradientOrigin");
340
0
  char *radius_x_att = fz_xml_att(root, "RadiusX");
341
0
  char *radius_y_att = fz_xml_att(root, "RadiusY");
342
343
0
  x0 = y0 = 0.0f;
344
0
  x1 = y1 = 1.0f;
345
0
  xrad = 1.0f;
346
0
  yrad = 1.0f;
347
348
0
  if (origin_att)
349
0
    xps_parse_point(ctx, doc, origin_att, &x0, &y0);
350
0
  if (center_att)
351
0
    xps_parse_point(ctx, doc, center_att, &x1, &y1);
352
0
  if (radius_x_att)
353
0
    xrad = fz_atof(radius_x_att);
354
0
  if (radius_y_att)
355
0
    yrad = fz_atof(radius_y_att);
356
357
0
  xrad = fz_max(0.01f, xrad);
358
0
  yrad = fz_max(0.01f, yrad);
359
360
  /* scale the ctm to make ellipses */
361
0
  if (fz_abs(xrad) > FLT_EPSILON)
362
0
  {
363
0
    ctm = fz_pre_scale(ctm, 1, yrad/xrad);
364
0
  }
365
366
0
  if (yrad != 0.0f)
367
0
  {
368
0
    invscale = xrad / yrad;
369
0
    y0 = y0 * invscale;
370
0
    y1 = y1 * invscale;
371
0
  }
372
373
0
  r0 = 0;
374
0
  r1 = xrad;
375
376
0
  inv = fz_invert_matrix(ctm);
377
0
  area = fz_transform_rect(area, inv);
378
0
  ma = fz_maxi(ma, ceilf(hypotf(area.x0 - x0, area.y0 - y0) / xrad));
379
0
  ma = fz_maxi(ma, ceilf(hypotf(area.x1 - x0, area.y0 - y0) / xrad));
380
0
  ma = fz_maxi(ma, ceilf(hypotf(area.x0 - x0, area.y1 - y0) / xrad));
381
0
  ma = fz_maxi(ma, ceilf(hypotf(area.x1 - x0, area.y1 - y0) / xrad));
382
383
0
  if (spread == SPREAD_REPEAT)
384
0
  {
385
0
    for (i = ma - 1; i >= 0; i--)
386
0
      xps_draw_one_radial_gradient(ctx, doc, ctm, stops, count, 0, x0, y0, r0 + i * xrad, x1, y1, r1 + i * xrad);
387
0
  }
388
0
  else if (spread == SPREAD_REFLECT)
389
0
  {
390
0
    if ((ma % 2) != 0)
391
0
      ma++;
392
0
    for (i = ma - 2; i >= 0; i -= 2)
393
0
    {
394
0
      xps_draw_one_radial_gradient(ctx, doc, ctm, stops, count, 0, x0, y0, r0 + i * xrad, x1, y1, r1 + i * xrad);
395
0
      xps_draw_one_radial_gradient(ctx, doc, ctm, stops, count, 0, x0, y0, r0 + (i + 2) * xrad, x1, y1, r1 + i * xrad);
396
0
    }
397
0
  }
398
0
  else
399
0
  {
400
0
    xps_draw_one_radial_gradient(ctx, doc, ctm, stops, count, 1, x0, y0, r0, x1, y1, r1);
401
0
  }
402
0
}
403
404
/*
405
 * Calculate how many iterations are needed to cover
406
 * the bounding box.
407
 */
408
409
static void
410
xps_draw_linear_gradient(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
411
  struct stop *stops, int count,
412
  fz_xml *root, int spread)
413
0
{
414
0
  float x0, y0, x1, y1;
415
0
  int i, mi, ma;
416
0
  float dx, dy, x, y, k;
417
0
  fz_point p1, p2;
418
0
  fz_matrix inv;
419
420
0
  char *start_point_att = fz_xml_att(root, "StartPoint");
421
0
  char *end_point_att = fz_xml_att(root, "EndPoint");
422
423
0
  x0 = y0 = 0;
424
0
  x1 = y1 = 1;
425
426
0
  if (start_point_att)
427
0
    xps_parse_point(ctx, doc, start_point_att, &x0, &y0);
428
0
  if (end_point_att)
429
0
    xps_parse_point(ctx, doc, end_point_att, &x1, &y1);
430
431
0
  p1.x = x0; p1.y = y0; p2.x = x1; p2.y = y1;
432
0
  inv = fz_invert_matrix(ctm);
433
0
  area = fz_transform_rect(area, inv);
434
0
  x = p2.x - p1.x; y = p2.y - p1.y;
435
0
  k = ((area.x0 - p1.x) * x + (area.y0 - p1.y) * y) / (x * x + y * y);
436
0
  mi = floorf(k); ma = ceilf(k);
437
0
  k = ((area.x1 - p1.x) * x + (area.y0 - p1.y) * y) / (x * x + y * y);
438
0
  mi = fz_mini(mi, floorf(k)); ma = fz_maxi(ma, ceilf(k));
439
0
  k = ((area.x0 - p1.x) * x + (area.y1 - p1.y) * y) / (x * x + y * y);
440
0
  mi = fz_mini(mi, floorf(k)); ma = fz_maxi(ma, ceilf(k));
441
0
  k = ((area.x1 - p1.x) * x + (area.y1 - p1.y) * y) / (x * x + y * y);
442
0
  mi = fz_mini(mi, floorf(k)); ma = fz_maxi(ma, ceilf(k));
443
0
  dx = x1 - x0; dy = y1 - y0;
444
445
0
  if (spread == SPREAD_REPEAT)
446
0
  {
447
0
    for (i = mi; i < ma; i++)
448
0
      xps_draw_one_linear_gradient(ctx, doc, ctm, stops, count, 0, x0 + i * dx, y0 + i * dy, x1 + i * dx, y1 + i * dy);
449
0
  }
450
0
  else if (spread == SPREAD_REFLECT)
451
0
  {
452
0
    if ((mi % 2) != 0)
453
0
      mi--;
454
0
    for (i = mi; i < ma; i += 2)
455
0
    {
456
0
      xps_draw_one_linear_gradient(ctx, doc, ctm, stops, count, 0, x0 + i * dx, y0 + i * dy, x1 + i * dx, y1 + i * dy);
457
0
      xps_draw_one_linear_gradient(ctx, doc, ctm, stops, count, 0, x0 + (i + 2) * dx, y0 + (i + 2) * dy, x1 + i * dx, y1 + i * dy);
458
0
    }
459
0
  }
460
0
  else
461
0
  {
462
0
    xps_draw_one_linear_gradient(ctx, doc, ctm, stops, count, 1, x0, y0, x1, y1);
463
0
  }
464
0
}
465
466
/*
467
 * Parse XML tag and attributes for a gradient brush, create color/opacity
468
 * function objects and call gradient drawing primitives.
469
 */
470
471
static void
472
xps_parse_gradient_brush(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
473
  char *base_uri, xps_resource *dict, fz_xml *root,
474
  void (*draw)(fz_context *ctx, xps_document *, fz_matrix, fz_rect, struct stop *, int, fz_xml *, int))
475
0
{
476
0
  fz_xml *node;
477
478
0
  char *opacity_att;
479
0
  char *spread_att;
480
0
  char *transform_att;
481
482
0
  fz_xml *transform_tag = NULL;
483
0
  fz_xml *stop_tag = NULL;
484
485
0
  struct stop stop_list[MAX_STOPS];
486
0
  int stop_count;
487
0
  int spread_method;
488
489
0
  opacity_att = fz_xml_att(root, "Opacity");
490
0
  spread_att = fz_xml_att(root, "SpreadMethod");
491
0
  transform_att = fz_xml_att(root, "Transform");
492
493
0
  for (node = fz_xml_down(root); node; node = fz_xml_next(node))
494
0
  {
495
0
    if (fz_xml_is_tag(node, "LinearGradientBrush.Transform"))
496
0
      transform_tag = fz_xml_down(node);
497
0
    if (fz_xml_is_tag(node, "RadialGradientBrush.Transform"))
498
0
      transform_tag = fz_xml_down(node);
499
0
    if (fz_xml_is_tag(node, "LinearGradientBrush.GradientStops"))
500
0
      stop_tag = fz_xml_down(node);
501
0
    if (fz_xml_is_tag(node, "RadialGradientBrush.GradientStops"))
502
0
      stop_tag = fz_xml_down(node);
503
0
  }
504
505
0
  xps_resolve_resource_reference(ctx, doc, dict, &transform_att, &transform_tag, NULL);
506
507
0
  spread_method = SPREAD_PAD;
508
0
  if (spread_att)
509
0
  {
510
0
    if (!strcmp(spread_att, "Pad"))
511
0
      spread_method = SPREAD_PAD;
512
0
    if (!strcmp(spread_att, "Reflect"))
513
0
      spread_method = SPREAD_REFLECT;
514
0
    if (!strcmp(spread_att, "Repeat"))
515
0
      spread_method = SPREAD_REPEAT;
516
0
  }
517
518
0
  ctm = xps_parse_transform(ctx, doc, transform_att, transform_tag, ctm);
519
520
0
  if (!stop_tag) {
521
0
    fz_warn(ctx, "missing gradient stops tag");
522
0
    return;
523
0
  }
524
525
0
  stop_count = xps_parse_gradient_stops(ctx, doc, base_uri, stop_tag, stop_list, MAX_STOPS);
526
0
  if (stop_count == 0)
527
0
  {
528
0
    fz_warn(ctx, "no gradient stops found");
529
0
    return;
530
0
  }
531
532
0
  xps_begin_opacity(ctx, doc, ctm, area, base_uri, dict, opacity_att, NULL);
533
534
0
  draw(ctx, doc, ctm, area, stop_list, stop_count, root, spread_method);
535
536
0
  xps_end_opacity(ctx, doc, base_uri, dict, opacity_att, NULL);
537
0
}
538
539
void
540
xps_parse_linear_gradient_brush(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
541
  char *base_uri, xps_resource *dict, fz_xml *root)
542
0
{
543
0
  xps_parse_gradient_brush(ctx, doc, ctm, area, base_uri, dict, root, xps_draw_linear_gradient);
544
0
}
545
546
void
547
xps_parse_radial_gradient_brush(fz_context *ctx, xps_document *doc, fz_matrix ctm, fz_rect area,
548
  char *base_uri, xps_resource *dict, fz_xml *root)
549
0
{
550
0
  xps_parse_gradient_brush(ctx, doc, ctm, area, base_uri, dict, root, xps_draw_radial_gradient);
551
0
}