David PHAM-VAN

Add Gradient decoration

1 # Changelog 1 # Changelog
2 2
  3 +## 1.7.0
  4 +
  5 +- Implement Linear and Radial gradients in BoxDecoration
  6 +
3 ## 1.6.2 7 ## 1.6.2
4 8
5 - Use the Barcode library to generate QR-Codes 9 - Use the Barcode library to generate QR-Codes
@@ -18,7 +18,11 @@ @@ -18,7 +18,11 @@
18 18
19 part of pdf; 19 part of pdf;
20 20
21 -class PdfFunction extends PdfObjectStream { 21 +abstract class PdfBaseFunction extends PdfObject {
  22 + PdfBaseFunction(PdfDocument pdfDocument) : super(pdfDocument);
  23 +}
  24 +
  25 +class PdfFunction extends PdfObjectStream implements PdfBaseFunction {
22 PdfFunction( 26 PdfFunction(
23 PdfDocument pdfDocument, { 27 PdfDocument pdfDocument, {
24 this.colors, 28 this.colors,
@@ -43,6 +47,39 @@ class PdfFunction extends PdfObjectStream { @@ -43,6 +47,39 @@ class PdfFunction extends PdfObjectStream {
43 params['/Order'] = const PdfNum(3); 47 params['/Order'] = const PdfNum(3);
44 params['/Domain'] = PdfArray.fromNum(const <num>[0, 1]); 48 params['/Domain'] = PdfArray.fromNum(const <num>[0, 1]);
45 params['/Range'] = PdfArray.fromNum(const <num>[0, 1, 0, 1, 0, 1]); 49 params['/Range'] = PdfArray.fromNum(const <num>[0, 1, 0, 1, 0, 1]);
46 - params['/Size'] = PdfNum(colors.length); 50 + params['/Size'] = PdfArray.fromNum(<int>[colors.length]);
  51 + }
  52 +}
  53 +
  54 +class PdfStitchingFunction extends PdfBaseFunction {
  55 + PdfStitchingFunction(
  56 + PdfDocument pdfDocument, {
  57 + @required this.functions,
  58 + @required this.bounds,
  59 + this.domainStart = 0,
  60 + this.domainEnd = 1,
  61 + }) : assert(functions != null),
  62 + assert(bounds != null),
  63 + super(pdfDocument);
  64 +
  65 + final List<PdfFunction> functions;
  66 +
  67 + final List<double> bounds;
  68 +
  69 + final double domainStart;
  70 +
  71 + final double domainEnd;
  72 +
  73 + @override
  74 + void _prepare() {
  75 + super._prepare();
  76 +
  77 + params['/FunctionType'] = const PdfNum(3);
  78 + params['/Functions'] = PdfArray.fromObjects(functions);
  79 + params['/Order'] = const PdfNum(3);
  80 + params['/Domain'] = PdfArray.fromNum(<num>[domainStart, domainEnd]);
  81 + params['/Bounds'] = PdfArray.fromNum(bounds);
  82 + params['/Encode'] = PdfArray.fromNum(
  83 + List<int>.generate(functions.length * 2, (int i) => i % 2));
47 } 84 }
48 } 85 }
@@ -45,9 +45,16 @@ class PdfObjectStream extends PdfObject { @@ -45,9 +45,16 @@ class PdfObjectStream extends PdfObject {
45 // The data is already in the right format 45 // The data is already in the right format
46 _data = buf.output(); 46 _data = buf.output();
47 } else if (pdfDocument.deflate != null) { 47 } else if (pdfDocument.deflate != null) {
48 - _data = pdfDocument.deflate(buf.output()); 48 + final Uint8List original = buf.output();
  49 + final Uint8List newData = pdfDocument.deflate(original);
  50 + if (newData.lengthInBytes < original.lengthInBytes) {
49 params['/Filter'] = const PdfName('/FlateDecode'); 51 params['/Filter'] = const PdfName('/FlateDecode');
50 - } else if (isBinary) { 52 + _data = newData;
  53 + }
  54 + }
  55 +
  56 + if (_data == null) {
  57 + if (isBinary) {
51 // This is a Ascii85 stream 58 // This is a Ascii85 stream
52 final Ascii85Encoder e = Ascii85Encoder(); 59 final Ascii85Encoder e = Ascii85Encoder();
53 _data = e.convert(buf.output()); 60 _data = e.convert(buf.output());
@@ -56,6 +63,7 @@ class PdfObjectStream extends PdfObject { @@ -56,6 +63,7 @@ class PdfObjectStream extends PdfObject {
56 // This is a non-deflated stream 63 // This is a non-deflated stream
57 _data = buf.output(); 64 _data = buf.output();
58 } 65 }
  66 + }
59 if (pdfDocument.encryption != null) { 67 if (pdfDocument.encryption != null) {
60 _data = pdfDocument.encryption.encrypt(_data, this); 68 _data = pdfDocument.encryption.encrypt(_data, this);
61 } 69 }
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 18
19 part of pdf; 19 part of pdf;
20 20
21 -enum PdfShadingType { function, axial, radial } 21 +enum PdfShadingType { axial, radial }
22 22
23 class PdfShading extends PdfObject { 23 class PdfShading extends PdfObject {
24 PdfShading( 24 PdfShading(
@@ -27,10 +27,17 @@ class PdfShading extends PdfObject { @@ -27,10 +27,17 @@ class PdfShading extends PdfObject {
27 @required this.function, 27 @required this.function,
28 @required this.start, 28 @required this.start,
29 @required this.end, 29 @required this.end,
  30 + this.radius0,
  31 + this.radius1,
  32 + this.boundingBox,
  33 + this.extendStart = false,
  34 + this.extendEnd = false,
30 }) : assert(shadingType != null), 35 }) : assert(shadingType != null),
31 assert(function != null), 36 assert(function != null),
32 assert(start != null), 37 assert(start != null),
33 assert(end != null), 38 assert(end != null),
  39 + assert(extendStart != null),
  40 + assert(extendEnd != null),
34 super(pdfDocument); 41 super(pdfDocument);
35 42
36 /// Name of the Shading object 43 /// Name of the Shading object
@@ -38,23 +45,52 @@ class PdfShading extends PdfObject { @@ -38,23 +45,52 @@ class PdfShading extends PdfObject {
38 45
39 final PdfShadingType shadingType; 46 final PdfShadingType shadingType;
40 47
41 - final PdfFunction function; 48 + final PdfBaseFunction function;
42 49
43 final PdfPoint start; 50 final PdfPoint start;
44 51
45 final PdfPoint end; 52 final PdfPoint end;
46 53
  54 + final PdfRect boundingBox;
  55 +
  56 + final bool extendStart;
  57 +
  58 + final bool extendEnd;
  59 +
  60 + final double radius0;
  61 +
  62 + final double radius1;
  63 +
47 @override 64 @override
48 void _prepare() { 65 void _prepare() {
49 super._prepare(); 66 super._prepare();
50 67
51 - params['/ShadingType'] = PdfNum(shadingType.index + 1); 68 + params['/ShadingType'] = PdfNum(shadingType.index + 2);
  69 + if (boundingBox != null) {
  70 + params['/BBox'] = PdfArray.fromNum(<double>[
  71 + boundingBox.left,
  72 + boundingBox.bottom,
  73 + boundingBox.right,
  74 + boundingBox.top,
  75 + ]);
  76 + }
52 params['/AntiAlias'] = const PdfBool(true); 77 params['/AntiAlias'] = const PdfBool(true);
53 params['/ColorSpace'] = const PdfName('/DeviceRGB'); 78 params['/ColorSpace'] = const PdfName('/DeviceRGB');
  79 +
  80 + if (shadingType == PdfShadingType.axial) {
54 params['/Coords'] = 81 params['/Coords'] =
55 PdfArray.fromNum(<double>[start.x, start.y, end.x, end.y]); 82 PdfArray.fromNum(<double>[start.x, start.y, end.x, end.y]);
56 - params['/Domain'] = PdfArray.fromNum(<num>[0, 1]);  
57 - params['/Extend'] = PdfArray(const <PdfBool>[PdfBool(true), PdfBool(true)]); 83 + } else if (shadingType == PdfShadingType.radial) {
  84 + assert(radius0 != null);
  85 + assert(radius1 != null);
  86 + params['/Coords'] = PdfArray.fromNum(
  87 + <double>[start.x, start.y, radius0, end.x, end.y, radius1]);
  88 + }
  89 + // params['/Domain'] = PdfArray.fromNum(<num>[0, 1]);
  90 + if (extendStart || extendEnd) {
  91 + params['/Extend'] =
  92 + PdfArray(<PdfBool>[PdfBool(extendStart), PdfBool(extendEnd)]);
  93 + }
58 params['/Function'] = function.ref(); 94 params['/Function'] = function.ref();
59 } 95 }
60 } 96 }
@@ -155,6 +155,198 @@ class DecorationImage { @@ -155,6 +155,198 @@ class DecorationImage {
155 } 155 }
156 } 156 }
157 157
  158 +/// Defines what happens at the edge of the gradient.
  159 +enum TileMode {
  160 + /// Edge is clamped to the final color.
  161 + clamp,
  162 +
  163 + /// Edge is repeated from first color to last.
  164 + // repeated,
  165 +
  166 + /// Edge is mirrored from last color to first.
  167 + // mirror,
  168 +}
  169 +
  170 +/// A 2D gradient.
  171 +@immutable
  172 +abstract class Gradient {
  173 + /// Initialize the gradient's colors and stops.
  174 + const Gradient({
  175 + @required this.colors,
  176 + this.stops,
  177 + }) : assert(colors != null);
  178 +
  179 + final List<PdfColor> colors;
  180 +
  181 + /// A list of values from 0.0 to 1.0 that denote fractions along the gradient.
  182 + final List<double> stops;
  183 +
  184 + PdfBaseFunction _buildFunction(
  185 + Context context,
  186 + List<PdfColor> colors,
  187 + List<double> stops,
  188 + ) {
  189 + if (stops == null) {
  190 + return PdfFunction(
  191 + context.document,
  192 + colors: colors,
  193 + );
  194 + }
  195 +
  196 + final List<PdfFunction> fn = <PdfFunction>[];
  197 +
  198 + PdfColor lc = colors.first;
  199 + for (final PdfColor c in colors.sublist(1)) {
  200 + fn.add(PdfFunction(
  201 + context.document,
  202 + colors: <PdfColor>[lc, c],
  203 + ));
  204 + lc = c;
  205 + }
  206 +
  207 + return PdfStitchingFunction(
  208 + context.document,
  209 + functions: fn,
  210 + bounds: stops.sublist(1, stops.length - 1),
  211 + domainStart: stops.first,
  212 + domainEnd: stops.last,
  213 + );
  214 + }
  215 +
  216 + void paint(Context context, PdfRect box);
  217 +}
  218 +
  219 +/// A 2D linear gradient.
  220 +class LinearGradient extends Gradient {
  221 + /// Creates a linear gradient.
  222 + const LinearGradient({
  223 + this.begin = Alignment.centerLeft,
  224 + this.end = Alignment.centerRight,
  225 + @required List<PdfColor> colors,
  226 + List<double> stops,
  227 + this.tileMode = TileMode.clamp,
  228 + }) : assert(begin != null),
  229 + assert(end != null),
  230 + assert(tileMode != null),
  231 + super(colors: colors, stops: stops);
  232 +
  233 + /// The offset at which stop 0.0 of the gradient is placed.
  234 + final Alignment begin;
  235 +
  236 + /// The offset at which stop 1.0 of the gradient is placed.
  237 + final Alignment end;
  238 +
  239 + /// How this gradient should tile the plane beyond in the region before
  240 + final TileMode tileMode;
  241 +
  242 + @override
  243 + void paint(Context context, PdfRect box) {
  244 + if (colors.isEmpty) {
  245 + return;
  246 + }
  247 +
  248 + if (colors.length == 1) {
  249 + context.canvas
  250 + ..setFillColor(colors.first)
  251 + ..fillPath();
  252 + }
  253 +
  254 + assert(stops == null || stops.length == colors.length);
  255 +
  256 + context.canvas
  257 + ..saveContext()
  258 + ..clipPath()
  259 + ..applyShader(
  260 + PdfShading(
  261 + context.document,
  262 + shadingType: PdfShadingType.axial,
  263 + boundingBox: box,
  264 + function: _buildFunction(context, colors, stops),
  265 + start: begin.withinRect(box),
  266 + end: end.withinRect(box),
  267 + extendStart: true,
  268 + extendEnd: true,
  269 + ),
  270 + )
  271 + ..restoreContext();
  272 + }
  273 +}
  274 +
  275 +/// A 2D radial gradient.
  276 +class RadialGradient extends Gradient {
  277 + /// Creates a radial gradient.
  278 + ///
  279 + /// The [colors] argument must not be null. If [stops] is non-null, it must
  280 + /// have the same length as [colors].
  281 + const RadialGradient({
  282 + this.center = Alignment.center,
  283 + this.radius = 0.5,
  284 + @required List<PdfColor> colors,
  285 + List<double> stops,
  286 + this.tileMode = TileMode.clamp,
  287 + this.focal,
  288 + this.focalRadius = 0.0,
  289 + }) : assert(center != null),
  290 + assert(radius != null),
  291 + assert(tileMode != null),
  292 + assert(focalRadius != null),
  293 + super(colors: colors, stops: stops);
  294 +
  295 + /// The center of the gradient
  296 + final Alignment center;
  297 +
  298 + /// The radius of the gradient
  299 + final double radius;
  300 +
  301 + /// How this gradient should tile the plane beyond the outer ring at [radius]
  302 + /// pixels from the [center].
  303 + final TileMode tileMode;
  304 +
  305 + /// The focal point of the gradient.
  306 + final Alignment focal;
  307 +
  308 + /// The radius of the focal point of the gradient.
  309 + final double focalRadius;
  310 +
  311 + @override
  312 + void paint(Context context, PdfRect box) {
  313 + if (colors.isEmpty) {
  314 + return;
  315 + }
  316 +
  317 + if (colors.length == 1) {
  318 + context.canvas
  319 + ..setFillColor(colors.first)
  320 + ..fillPath();
  321 + }
  322 +
  323 + assert(stops == null || stops.length == colors.length);
  324 +
  325 + final Alignment _focal = focal ?? center;
  326 +
  327 + final double _radius = math.min(box.width, box.height);
  328 +
  329 + context.canvas
  330 + ..saveContext()
  331 + ..clipPath()
  332 + ..applyShader(
  333 + PdfShading(
  334 + context.document,
  335 + shadingType: PdfShadingType.radial,
  336 + boundingBox: box,
  337 + function: _buildFunction(context, colors, stops),
  338 + start: _focal.withinRect(box),
  339 + end: center.withinRect(box),
  340 + radius0: focalRadius * _radius,
  341 + radius1: radius * _radius,
  342 + extendStart: true,
  343 + extendEnd: true,
  344 + ),
  345 + )
  346 + ..restoreContext();
  347 + }
  348 +}
  349 +
158 enum BoxShape { circle, rectangle } 350 enum BoxShape { circle, rectangle }
159 351
160 @immutable 352 @immutable
@@ -163,6 +355,7 @@ class BoxDecoration { @@ -163,6 +355,7 @@ class BoxDecoration {
163 {this.color, 355 {this.color,
164 this.border, 356 this.border,
165 this.borderRadius, 357 this.borderRadius,
  358 + this.gradient,
166 this.image, 359 this.image,
167 this.shape = BoxShape.rectangle}) 360 this.shape = BoxShape.rectangle})
168 : assert(shape != null); 361 : assert(shape != null);
@@ -173,6 +366,7 @@ class BoxDecoration { @@ -173,6 +366,7 @@ class BoxDecoration {
173 final double borderRadius; 366 final double borderRadius;
174 final BoxShape shape; 367 final BoxShape shape;
175 final DecorationImage image; 368 final DecorationImage image;
  369 + final Gradient gradient;
176 370
177 void paint(Context context, PdfRect box) { 371 void paint(Context context, PdfRect box) {
178 assert(box.x != null); 372 assert(box.x != null);
@@ -200,6 +394,25 @@ class BoxDecoration { @@ -200,6 +394,25 @@ class BoxDecoration {
200 ..fillPath(); 394 ..fillPath();
201 } 395 }
202 396
  397 + if (gradient != null) {
  398 + switch (shape) {
  399 + case BoxShape.rectangle:
  400 + if (borderRadius == null) {
  401 + context.canvas.drawRect(box.x, box.y, box.width, box.height);
  402 + } else {
  403 + context.canvas.drawRRect(box.x, box.y, box.width, box.height,
  404 + borderRadius, borderRadius);
  405 + }
  406 + break;
  407 + case BoxShape.circle:
  408 + context.canvas.drawEllipse(box.x + box.width / 2.0,
  409 + box.y + box.height / 2.0, box.width / 2.0, box.height / 2.0);
  410 + break;
  411 + }
  412 +
  413 + gradient.paint(context, box);
  414 + }
  415 +
203 if (image != null) { 416 if (image != null) {
204 context.canvas.saveContext(); 417 context.canvas.saveContext();
205 switch (shape) { 418 switch (shape) {
@@ -332,7 +332,7 @@ class Alignment { @@ -332,7 +332,7 @@ class Alignment {
332 final double halfHeight = rect.height / 2.0; 332 final double halfHeight = rect.height / 2.0;
333 return PdfPoint( 333 return PdfPoint(
334 rect.left + halfWidth + x * halfWidth, 334 rect.left + halfWidth + x * halfWidth,
335 - rect.top + halfHeight + y * halfHeight, 335 + rect.bottom + halfHeight + y * halfHeight,
336 ); 336 );
337 } 337 }
338 338
@@ -353,6 +353,16 @@ class Alignment { @@ -353,6 +353,16 @@ class Alignment {
353 String toString() => '($x, $y)'; 353 String toString() => '($x, $y)';
354 } 354 }
355 355
  356 +/// An offset that's expressed as a fraction of a [PdfPoint].
  357 +@immutable
  358 +class FractionalOffset extends Alignment {
  359 + /// Creates a fractional offset.
  360 + const FractionalOffset(double dx, double dy)
  361 + : assert(dx != null),
  362 + assert(dy != null),
  363 + super(dx * 2 - 1, 1 - dy * 2);
  364 +}
  365 +
356 /// The pair of sizes returned by [applyBoxFit]. 366 /// The pair of sizes returned by [applyBoxFit].
357 @immutable 367 @immutable
358 class FittedSizes { 368 class FittedSizes {
@@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl @@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl
4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf 4 homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf
5 repository: https://github.com/DavBfr/dart_pdf 5 repository: https://github.com/DavBfr/dart_pdf
6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues 6 issue_tracker: https://github.com/DavBfr/dart_pdf/issues
7 -version: 1.6.2 7 +version: 1.7.0
8 8
9 environment: 9 environment:
10 sdk: ">=2.3.0 <3.0.0" 10 sdk: ">=2.3.0 <3.0.0"
@@ -158,6 +158,73 @@ void main() { @@ -158,6 +158,73 @@ void main() {
158 )); 158 ));
159 }); 159 });
160 160
  161 + test('Container Widgets LinearGradient', () {
  162 + pdf.addPage(Page(
  163 + build: (Context context) => Container(
  164 + alignment: Alignment.center,
  165 + margin: const EdgeInsets.all(30),
  166 + padding: const EdgeInsets.all(20),
  167 + decoration: const BoxDecoration(
  168 + borderRadius: 20,
  169 + gradient: LinearGradient(
  170 + colors: <PdfColor>[
  171 + PdfColors.blue,
  172 + PdfColors.red,
  173 + PdfColors.yellow,
  174 + ],
  175 + begin: Alignment.bottomLeft,
  176 + end: Alignment.topRight,
  177 + stops: <double>[0, .8, 1.0],
  178 + tileMode: TileMode.clamp,
  179 + ),
  180 + border: BoxBorder(
  181 + color: PdfColors.blue800,
  182 + top: true,
  183 + left: true,
  184 + right: true,
  185 + bottom: true,
  186 + width: 2,
  187 + )),
  188 + width: 200,
  189 + height: 400,
  190 + ),
  191 + ));
  192 + });
  193 +
  194 + test('Container Widgets RadialGradient', () {
  195 + pdf.addPage(Page(
  196 + build: (Context context) => Container(
  197 + alignment: Alignment.center,
  198 + margin: const EdgeInsets.all(30),
  199 + padding: const EdgeInsets.all(20),
  200 + decoration: const BoxDecoration(
  201 + borderRadius: 20,
  202 + gradient: RadialGradient(
  203 + colors: <PdfColor>[
  204 + PdfColors.blue,
  205 + PdfColors.red,
  206 + PdfColors.yellow,
  207 + ],
  208 + stops: <double>[0.0, .2, 1.0],
  209 + center: FractionalOffset(.7, .2),
  210 + focal: FractionalOffset(.7, .45),
  211 + focalRadius: 1,
  212 + ),
  213 + border: BoxBorder(
  214 + color: PdfColors.blue800,
  215 + top: true,
  216 + left: true,
  217 + right: true,
  218 + bottom: true,
  219 + width: 2,
  220 + )),
  221 + width: 200,
  222 + height: 400,
  223 + // child: Placeholder(),
  224 + ),
  225 + ));
  226 + });
  227 +
161 tearDownAll(() { 228 tearDownAll(() {
162 final File file = File('widgets-container.pdf'); 229 final File file = File('widgets-container.pdf');
163 file.writeAsBytesSync(pdf.save()); 230 file.writeAsBytesSync(pdf.save());