David PHAM-VAN

Add Gradient decoration

# Changelog
## 1.7.0
- Implement Linear and Radial gradients in BoxDecoration
## 1.6.2
- Use the Barcode library to generate QR-Codes
... ...
... ... @@ -18,7 +18,11 @@
part of pdf;
class PdfFunction extends PdfObjectStream {
abstract class PdfBaseFunction extends PdfObject {
PdfBaseFunction(PdfDocument pdfDocument) : super(pdfDocument);
}
class PdfFunction extends PdfObjectStream implements PdfBaseFunction {
PdfFunction(
PdfDocument pdfDocument, {
this.colors,
... ... @@ -43,6 +47,39 @@ class PdfFunction extends PdfObjectStream {
params['/Order'] = const PdfNum(3);
params['/Domain'] = PdfArray.fromNum(const <num>[0, 1]);
params['/Range'] = PdfArray.fromNum(const <num>[0, 1, 0, 1, 0, 1]);
params['/Size'] = PdfNum(colors.length);
params['/Size'] = PdfArray.fromNum(<int>[colors.length]);
}
}
class PdfStitchingFunction extends PdfBaseFunction {
PdfStitchingFunction(
PdfDocument pdfDocument, {
@required this.functions,
@required this.bounds,
this.domainStart = 0,
this.domainEnd = 1,
}) : assert(functions != null),
assert(bounds != null),
super(pdfDocument);
final List<PdfFunction> functions;
final List<double> bounds;
final double domainStart;
final double domainEnd;
@override
void _prepare() {
super._prepare();
params['/FunctionType'] = const PdfNum(3);
params['/Functions'] = PdfArray.fromObjects(functions);
params['/Order'] = const PdfNum(3);
params['/Domain'] = PdfArray.fromNum(<num>[domainStart, domainEnd]);
params['/Bounds'] = PdfArray.fromNum(bounds);
params['/Encode'] = PdfArray.fromNum(
List<int>.generate(functions.length * 2, (int i) => i % 2));
}
}
... ...
... ... @@ -45,16 +45,24 @@ class PdfObjectStream extends PdfObject {
// The data is already in the right format
_data = buf.output();
} else if (pdfDocument.deflate != null) {
_data = pdfDocument.deflate(buf.output());
params['/Filter'] = const PdfName('/FlateDecode');
} else if (isBinary) {
// This is a Ascii85 stream
final Ascii85Encoder e = Ascii85Encoder();
_data = e.convert(buf.output());
params['/Filter'] = const PdfName('/ASCII85Decode');
} else {
// This is a non-deflated stream
_data = buf.output();
final Uint8List original = buf.output();
final Uint8List newData = pdfDocument.deflate(original);
if (newData.lengthInBytes < original.lengthInBytes) {
params['/Filter'] = const PdfName('/FlateDecode');
_data = newData;
}
}
if (_data == null) {
if (isBinary) {
// This is a Ascii85 stream
final Ascii85Encoder e = Ascii85Encoder();
_data = e.convert(buf.output());
params['/Filter'] = const PdfName('/ASCII85Decode');
} else {
// This is a non-deflated stream
_data = buf.output();
}
}
if (pdfDocument.encryption != null) {
_data = pdfDocument.encryption.encrypt(_data, this);
... ...
... ... @@ -18,7 +18,7 @@
part of pdf;
enum PdfShadingType { function, axial, radial }
enum PdfShadingType { axial, radial }
class PdfShading extends PdfObject {
PdfShading(
... ... @@ -27,10 +27,17 @@ class PdfShading extends PdfObject {
@required this.function,
@required this.start,
@required this.end,
this.radius0,
this.radius1,
this.boundingBox,
this.extendStart = false,
this.extendEnd = false,
}) : assert(shadingType != null),
assert(function != null),
assert(start != null),
assert(end != null),
assert(extendStart != null),
assert(extendEnd != null),
super(pdfDocument);
/// Name of the Shading object
... ... @@ -38,23 +45,52 @@ class PdfShading extends PdfObject {
final PdfShadingType shadingType;
final PdfFunction function;
final PdfBaseFunction function;
final PdfPoint start;
final PdfPoint end;
final PdfRect boundingBox;
final bool extendStart;
final bool extendEnd;
final double radius0;
final double radius1;
@override
void _prepare() {
super._prepare();
params['/ShadingType'] = PdfNum(shadingType.index + 1);
params['/ShadingType'] = PdfNum(shadingType.index + 2);
if (boundingBox != null) {
params['/BBox'] = PdfArray.fromNum(<double>[
boundingBox.left,
boundingBox.bottom,
boundingBox.right,
boundingBox.top,
]);
}
params['/AntiAlias'] = const PdfBool(true);
params['/ColorSpace'] = const PdfName('/DeviceRGB');
params['/Coords'] =
PdfArray.fromNum(<double>[start.x, start.y, end.x, end.y]);
params['/Domain'] = PdfArray.fromNum(<num>[0, 1]);
params['/Extend'] = PdfArray(const <PdfBool>[PdfBool(true), PdfBool(true)]);
if (shadingType == PdfShadingType.axial) {
params['/Coords'] =
PdfArray.fromNum(<double>[start.x, start.y, end.x, end.y]);
} else if (shadingType == PdfShadingType.radial) {
assert(radius0 != null);
assert(radius1 != null);
params['/Coords'] = PdfArray.fromNum(
<double>[start.x, start.y, radius0, end.x, end.y, radius1]);
}
// params['/Domain'] = PdfArray.fromNum(<num>[0, 1]);
if (extendStart || extendEnd) {
params['/Extend'] =
PdfArray(<PdfBool>[PdfBool(extendStart), PdfBool(extendEnd)]);
}
params['/Function'] = function.ref();
}
}
... ...
... ... @@ -155,6 +155,198 @@ class DecorationImage {
}
}
/// Defines what happens at the edge of the gradient.
enum TileMode {
/// Edge is clamped to the final color.
clamp,
/// Edge is repeated from first color to last.
// repeated,
/// Edge is mirrored from last color to first.
// mirror,
}
/// A 2D gradient.
@immutable
abstract class Gradient {
/// Initialize the gradient's colors and stops.
const Gradient({
@required this.colors,
this.stops,
}) : assert(colors != null);
final List<PdfColor> colors;
/// A list of values from 0.0 to 1.0 that denote fractions along the gradient.
final List<double> stops;
PdfBaseFunction _buildFunction(
Context context,
List<PdfColor> colors,
List<double> stops,
) {
if (stops == null) {
return PdfFunction(
context.document,
colors: colors,
);
}
final List<PdfFunction> fn = <PdfFunction>[];
PdfColor lc = colors.first;
for (final PdfColor c in colors.sublist(1)) {
fn.add(PdfFunction(
context.document,
colors: <PdfColor>[lc, c],
));
lc = c;
}
return PdfStitchingFunction(
context.document,
functions: fn,
bounds: stops.sublist(1, stops.length - 1),
domainStart: stops.first,
domainEnd: stops.last,
);
}
void paint(Context context, PdfRect box);
}
/// A 2D linear gradient.
class LinearGradient extends Gradient {
/// Creates a linear gradient.
const LinearGradient({
this.begin = Alignment.centerLeft,
this.end = Alignment.centerRight,
@required List<PdfColor> colors,
List<double> stops,
this.tileMode = TileMode.clamp,
}) : assert(begin != null),
assert(end != null),
assert(tileMode != null),
super(colors: colors, stops: stops);
/// The offset at which stop 0.0 of the gradient is placed.
final Alignment begin;
/// The offset at which stop 1.0 of the gradient is placed.
final Alignment end;
/// How this gradient should tile the plane beyond in the region before
final TileMode tileMode;
@override
void paint(Context context, PdfRect box) {
if (colors.isEmpty) {
return;
}
if (colors.length == 1) {
context.canvas
..setFillColor(colors.first)
..fillPath();
}
assert(stops == null || stops.length == colors.length);
context.canvas
..saveContext()
..clipPath()
..applyShader(
PdfShading(
context.document,
shadingType: PdfShadingType.axial,
boundingBox: box,
function: _buildFunction(context, colors, stops),
start: begin.withinRect(box),
end: end.withinRect(box),
extendStart: true,
extendEnd: true,
),
)
..restoreContext();
}
}
/// A 2D radial gradient.
class RadialGradient extends Gradient {
/// Creates a radial gradient.
///
/// The [colors] argument must not be null. If [stops] is non-null, it must
/// have the same length as [colors].
const RadialGradient({
this.center = Alignment.center,
this.radius = 0.5,
@required List<PdfColor> colors,
List<double> stops,
this.tileMode = TileMode.clamp,
this.focal,
this.focalRadius = 0.0,
}) : assert(center != null),
assert(radius != null),
assert(tileMode != null),
assert(focalRadius != null),
super(colors: colors, stops: stops);
/// The center of the gradient
final Alignment center;
/// The radius of the gradient
final double radius;
/// How this gradient should tile the plane beyond the outer ring at [radius]
/// pixels from the [center].
final TileMode tileMode;
/// The focal point of the gradient.
final Alignment focal;
/// The radius of the focal point of the gradient.
final double focalRadius;
@override
void paint(Context context, PdfRect box) {
if (colors.isEmpty) {
return;
}
if (colors.length == 1) {
context.canvas
..setFillColor(colors.first)
..fillPath();
}
assert(stops == null || stops.length == colors.length);
final Alignment _focal = focal ?? center;
final double _radius = math.min(box.width, box.height);
context.canvas
..saveContext()
..clipPath()
..applyShader(
PdfShading(
context.document,
shadingType: PdfShadingType.radial,
boundingBox: box,
function: _buildFunction(context, colors, stops),
start: _focal.withinRect(box),
end: center.withinRect(box),
radius0: focalRadius * _radius,
radius1: radius * _radius,
extendStart: true,
extendEnd: true,
),
)
..restoreContext();
}
}
enum BoxShape { circle, rectangle }
@immutable
... ... @@ -163,6 +355,7 @@ class BoxDecoration {
{this.color,
this.border,
this.borderRadius,
this.gradient,
this.image,
this.shape = BoxShape.rectangle})
: assert(shape != null);
... ... @@ -173,6 +366,7 @@ class BoxDecoration {
final double borderRadius;
final BoxShape shape;
final DecorationImage image;
final Gradient gradient;
void paint(Context context, PdfRect box) {
assert(box.x != null);
... ... @@ -200,6 +394,25 @@ class BoxDecoration {
..fillPath();
}
if (gradient != null) {
switch (shape) {
case BoxShape.rectangle:
if (borderRadius == null) {
context.canvas.drawRect(box.x, box.y, box.width, box.height);
} else {
context.canvas.drawRRect(box.x, box.y, box.width, box.height,
borderRadius, borderRadius);
}
break;
case BoxShape.circle:
context.canvas.drawEllipse(box.x + box.width / 2.0,
box.y + box.height / 2.0, box.width / 2.0, box.height / 2.0);
break;
}
gradient.paint(context, box);
}
if (image != null) {
context.canvas.saveContext();
switch (shape) {
... ...
... ... @@ -332,7 +332,7 @@ class Alignment {
final double halfHeight = rect.height / 2.0;
return PdfPoint(
rect.left + halfWidth + x * halfWidth,
rect.top + halfHeight + y * halfHeight,
rect.bottom + halfHeight + y * halfHeight,
);
}
... ... @@ -353,6 +353,16 @@ class Alignment {
String toString() => '($x, $y)';
}
/// An offset that's expressed as a fraction of a [PdfPoint].
@immutable
class FractionalOffset extends Alignment {
/// Creates a fractional offset.
const FractionalOffset(double dx, double dy)
: assert(dx != null),
assert(dy != null),
super(dx * 2 - 1, 1 - dy * 2);
}
/// The pair of sizes returned by [applyBoxFit].
@immutable
class FittedSizes {
... ...
... ... @@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl
homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf
repository: https://github.com/DavBfr/dart_pdf
issue_tracker: https://github.com/DavBfr/dart_pdf/issues
version: 1.6.2
version: 1.7.0
environment:
sdk: ">=2.3.0 <3.0.0"
... ...
... ... @@ -158,6 +158,73 @@ void main() {
));
});
test('Container Widgets LinearGradient', () {
pdf.addPage(Page(
build: (Context context) => Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(30),
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
borderRadius: 20,
gradient: LinearGradient(
colors: <PdfColor>[
PdfColors.blue,
PdfColors.red,
PdfColors.yellow,
],
begin: Alignment.bottomLeft,
end: Alignment.topRight,
stops: <double>[0, .8, 1.0],
tileMode: TileMode.clamp,
),
border: BoxBorder(
color: PdfColors.blue800,
top: true,
left: true,
right: true,
bottom: true,
width: 2,
)),
width: 200,
height: 400,
),
));
});
test('Container Widgets RadialGradient', () {
pdf.addPage(Page(
build: (Context context) => Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(30),
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
borderRadius: 20,
gradient: RadialGradient(
colors: <PdfColor>[
PdfColors.blue,
PdfColors.red,
PdfColors.yellow,
],
stops: <double>[0.0, .2, 1.0],
center: FractionalOffset(.7, .2),
focal: FractionalOffset(.7, .45),
focalRadius: 1,
),
border: BoxBorder(
color: PdfColors.blue800,
top: true,
left: true,
right: true,
bottom: true,
width: 2,
)),
width: 200,
height: 400,
// child: Placeholder(),
),
));
});
tearDownAll(() {
final File file = File('widgets-container.pdf');
file.writeAsBytesSync(pdf.save());
... ...