David PHAM-VAN

Add document outline support

... ... @@ -22,7 +22,7 @@ import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
Future<Uint8List> generateDocument(PdfPageFormat format) async {
final pw.Document doc = pw.Document();
final pw.Document doc = pw.Document(pageMode: PdfPageMode.outlines);
doc.addPage(pw.MultiPage(
pageFormat:
... ... @@ -57,6 +57,7 @@ Future<Uint8List> generateDocument(PdfPageFormat format) async {
build: (pw.Context context) => <pw.Widget>[
pw.Header(
level: 0,
title: 'Portable Document Format',
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: <pw.Widget>[
... ...
... ... @@ -4,6 +4,7 @@
- Implement different border radius on all corners
- Add AcroForm widgets
- Add document outline support
## 1.12.0
... ...
... ... @@ -207,20 +207,27 @@ class PdfSecString extends PdfString {
[PdfStringFormat format = PdfStringFormat.binary])
: super(value, format);
factory PdfSecString.fromString(PdfObject object, String value) {
factory PdfSecString.fromString(
PdfObject object,
String value, [
PdfStringFormat format = PdfStringFormat.litteral,
]) {
return PdfSecString(
object,
PdfString._string(value),
PdfStringFormat.litteral,
format,
);
}
factory PdfSecString.fromStream(PdfObject object, PdfStream value,
[PdfStringFormat format = PdfStringFormat.litteral]) {
factory PdfSecString.fromStream(
PdfObject object,
PdfStream value, [
PdfStringFormat format = PdfStringFormat.litteral,
]) {
return PdfSecString(
object,
value.output(),
PdfStringFormat.litteral,
format,
);
}
... ...
... ... @@ -14,13 +14,15 @@
* limitations under the License.
*/
// ignore_for_file: omit_local_variable_types
part of pdf;
class PdfNames extends PdfObject {
/// This constructs a Pdf Name object
PdfNames(PdfDocument pdfDocument) : super(pdfDocument);
final PdfArray _dests = PdfArray();
final Map<String, PdfDataType> _dests = <String, PdfDataType>{};
void addDest(
String name,
... ... @@ -32,8 +34,7 @@ class PdfNames extends PdfObject {
assert(page.pdfDocument == pdfDocument);
assert(name != null);
_dests.add(PdfSecString.fromString(this, name));
_dests.add(PdfDict(<String, PdfDataType>{
_dests[name] = PdfDict(<String, PdfDataType>{
'/D': PdfArray(<PdfDataType>[
page.ref(),
const PdfName('/XYZ'),
... ... @@ -41,13 +42,32 @@ class PdfNames extends PdfObject {
if (posY == null) const PdfNull() else PdfNum(posY),
if (posZ == null) const PdfNull() else PdfNum(posZ),
]),
}));
});
}
@override
void _prepare() {
super._prepare();
params['/Dests'] = PdfDict(<String, PdfDataType>{'/Names': _dests});
if (_dests.isEmpty) {
return;
}
final PdfArray dests = PdfArray();
final List<String> keys = _dests.keys.toList()..sort();
for (String name in keys) {
dests.add(PdfSecString.fromString(this, name));
dests.add(_dests[name]);
}
params['/Dests'] = PdfDict(<String, PdfDataType>{
'/Names': dests,
'/Limits': PdfArray(<PdfDataType>[
PdfSecString.fromString(this, keys.first),
PdfSecString.fromString(this, keys.last),
])
});
}
}
... ...
... ... @@ -26,15 +26,36 @@ enum PdfOutlineMode {
fitrect
}
enum PdfOutlineStyle {
/// Normal
normal,
/// Italic
italic,
// Bold
bold,
/// Italic and Bold
italicBold,
}
class PdfOutline extends PdfObject {
/// Constructs a Pdf Outline object. When selected, the specified region
/// is displayed.
///
/// @param title Title of the outline
/// @param dest The destination page
/// @param rect coordinate
PdfOutline(PdfDocument pdfDocument, {this.title, this.dest, this.rect})
: super(pdfDocument, '/Outlines');
PdfOutline(
PdfDocument pdfDocument, {
this.title,
this.dest,
this.rect,
this.anchor,
this.color,
this.destMode = PdfOutlineMode.fitpage,
this.style = PdfOutlineStyle.normal,
}) : assert(anchor == null || (dest == null && rect == null)),
assert(destMode != null),
assert(style != null),
super(pdfDocument);
/// This holds any outlines below us
List<PdfOutline> outlines = <PdfOutline>[];
... ... @@ -51,33 +72,25 @@ class PdfOutline extends PdfObject {
/// The region on the destination page
final PdfRect rect;
/// Named destination
final String anchor;
/// Color of the outline text
final PdfColor color;
/// How the destination is handled
PdfOutlineMode destMode = PdfOutlineMode.fitpage;
final PdfOutlineMode destMode;
/// How to display the outline text
final PdfOutlineStyle style;
int effectiveLevel;
/// This method creates an outline, and attaches it to this one.
/// When the outline is selected, the supplied region is displayed.
///
/// Note: the coordinates are in User space. They are converted to User
/// space.
///
/// This allows you to have an outline for say a Chapter,
/// then under the chapter, one for each section. You are not really
/// limited on how deep you go, but it's best not to go below say 6 levels,
/// for the reader's sake.
///
/// @param title Title of the outline
/// @param dest The destination page
/// @param x coordinate of region in User space
/// @param y coordinate of region in User space
/// @param w width of region in User space
/// @param h height of region in User space
/// @return [PdfOutline] object created, for creating sub-outlines
PdfOutline add({String title, PdfPage dest, PdfRect rect}) {
final PdfOutline outline =
PdfOutline(pdfDocument, title: title, dest: dest, rect: rect);
// Tell the outline of ourselves
void add(PdfOutline outline) {
outline.parent = this;
return outline;
outlines.add(outline);
}
/// @param os OutputStream to send the object to
... ... @@ -88,20 +101,33 @@ class PdfOutline extends PdfObject {
// These are for kids only
if (parent != null) {
params['/Title'] = PdfSecString.fromString(this, title);
final PdfArray dests = PdfArray();
dests.add(dest.ref());
if (destMode == PdfOutlineMode.fitpage) {
dests.add(const PdfName('/Fit'));
if (color != null) {
params['/C'] = PdfColorType(color);
}
if (style != PdfOutlineStyle.normal) {
params['/F'] = PdfNum(style.index);
}
if (anchor != null) {
params['/Dest'] = PdfSecString.fromString(this, anchor);
} else {
dests.add(const PdfName('/FitR'));
dests.add(PdfNum(rect.left));
dests.add(PdfNum(rect.bottom));
dests.add(PdfNum(rect.right));
dests.add(PdfNum(rect.top));
final PdfArray dests = PdfArray();
dests.add(dest.ref());
if (destMode == PdfOutlineMode.fitpage) {
dests.add(const PdfName('/Fit'));
} else {
dests.add(const PdfName('/FitR'));
dests.add(PdfNum(rect.left));
dests.add(PdfNum(rect.bottom));
dests.add(PdfNum(rect.right));
dests.add(PdfNum(rect.top));
}
params['/Dest'] = dests;
}
params['/Parent'] = parent.ref();
params['/Dest'] = dests;
// were a descendent, so by default we are closed. Find out how many
// entries are below us
... ...
... ... @@ -19,22 +19,38 @@
part of widget;
class Anchor extends SingleChildWidget {
Anchor({Widget child, @required this.name, this.description})
: assert(name != null),
Anchor({
Widget child,
@required this.name,
this.description,
this.zoom,
this.setX = false,
}) : assert(name != null),
assert(setX != null),
super(child: child);
final String name;
final String description;
final double zoom;
final bool setX;
@override
void paint(Context context) {
super.paint(context);
paintChild(context);
final Matrix4 mat = context.canvas.getTransform();
final Vector3 lt = mat.transform3(Vector3(box.left, box.bottom, 0));
context.document.pdfNames.addDest(name, context.page, posY: lt.y);
final Vector3 lt = mat.transform3(Vector3(box.left, box.top, 0));
context.document.pdfNames.addDest(
name,
context.page,
posX: setX ? lt.x : null,
posY: lt.y,
posZ: zoom,
);
if (description != null) {
final Vector3 rb = mat.transform3(Vector3(box.right, box.top, 0));
... ... @@ -307,3 +323,74 @@ class TextField extends Annotation {
textStyle: textStyle,
));
}
class Outline extends Anchor {
Outline({
Widget child,
@required String name,
@required this.title,
this.level = 0,
this.color,
this.style = PdfOutlineStyle.normal,
}) : assert(title != null),
assert(level != null && level >= 0),
assert(style != null),
super(child: child, name: name, setX: true);
final String title;
final int level;
final PdfColor color;
final PdfOutlineStyle style;
PdfOutline _outline;
@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
super.layout(context, constraints, parentUsesSize: parentUsesSize);
_buildOutline(context);
}
@override
void debugPaint(Context context) {
context.canvas
..setFillColor(PdfColors.pink100)
..drawRect(box.x, box.y, box.width, box.height)
..fillPath();
}
void _buildOutline(Context context) {
if (_outline != null) {
return;
}
_outline = PdfOutline(
context.document,
title: title,
anchor: name,
color: color,
style: style,
);
PdfOutline parent = context.document.outline;
int l = level;
while (l > 0) {
if (parent.effectiveLevel == l) {
break;
}
if (parent.outlines.isEmpty) {
parent.effectiveLevel = level;
break;
}
parent = parent.outlines.last;
l--;
}
parent.add(_outline);
}
}
... ...
... ... @@ -19,15 +19,23 @@
part of widget;
class Header extends StatelessWidget {
Header(
{this.level = 1,
this.text,
this.child,
this.decoration,
this.margin,
this.padding,
this.textStyle})
: assert(level >= 0 && level <= 5);
Header({
this.level = 1,
this.text,
this.child,
this.decoration,
this.margin,
this.padding,
this.textStyle,
String title,
this.outlineColor,
this.outlineStyle = PdfOutlineStyle.normal,
}) : assert(level != null, level >= 0 && level <= 5),
assert(text != null || child != null),
assert(outlineStyle != null),
title = title ?? text;
final String title;
final String text;
... ... @@ -43,6 +51,10 @@ class Header extends StatelessWidget {
final TextStyle textStyle;
final PdfColor outlineColor;
final PdfOutlineStyle outlineStyle;
@override
Widget build(Context context) {
BoxDecoration _decoration = decoration;
... ... @@ -85,13 +97,27 @@ class Header extends StatelessWidget {
_textStyle ??= Theme.of(context).header5;
break;
}
return Container(
final Widget container = Container(
alignment: Alignment.topLeft,
margin: _margin,
padding: _padding,
decoration: _decoration,
child: child ?? Text(text, style: _textStyle),
);
if (title == null) {
return container;
}
return Outline(
name: text.hashCode.toString(),
title: title,
child: container,
level: level,
color: outlineColor,
style: outlineStyle,
);
}
}
... ...
... ... @@ -40,6 +40,7 @@ import 'widget_form_test.dart' as widget_form;
import 'widget_grid_view_test.dart' as widget_grid_view;
import 'widget_multipage_test.dart' as widget_multipage;
import 'widget_opacity_test.dart' as widget_opacity;
import 'widget_outline_test.dart' as widget_outline;
import 'widget_partitions_test.dart' as widget_partitions;
import 'widget_table_test.dart' as widget_table;
import 'widget_test.dart' as widget;
... ... @@ -73,6 +74,7 @@ void main() {
widget_grid_view.main();
widget_multipage.main();
widget_opacity.main();
widget_outline.main();
widget_partitions.main();
widget_table.main();
widget_text.main();
... ...
... ... @@ -175,14 +175,14 @@ Document pdf;
void main() {
setUpAll(() {
Document.debug = true;
pdf = Document();
pdf = Document(pageMode: PdfPageMode.outlines);
});
test('Pdf Colors', () {
pdf.addPage(MultiPage(
pageFormat: PdfPageFormat.standard,
build: (Context context) => <Widget>[
Header(text: 'Red'),
Header(text: 'Red', outlineColor: PdfColors.red),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -207,7 +207,7 @@ void main() {
Color(PdfColors.redAccent700, 'Red', 'Accent 700'),
]),
NewPage(),
Header(text: 'Pink'),
Header(text: 'Pink', outlineColor: PdfColors.pink),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -232,7 +232,7 @@ void main() {
Color(PdfColors.pinkAccent700, 'Pink', 'Accent 700'),
]),
NewPage(),
Header(text: 'Purple'),
Header(text: 'Purple', outlineColor: PdfColors.purple),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -257,7 +257,7 @@ void main() {
Color(PdfColors.purpleAccent700, 'Purple', 'Accent 700'),
]),
NewPage(),
Header(text: 'Deep Purple'),
Header(text: 'Deep Purple', outlineColor: PdfColors.deepPurple),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -286,7 +286,7 @@ void main() {
'Accent 700'),
]),
NewPage(),
Header(text: 'Indigo'),
Header(text: 'Indigo', outlineColor: PdfColors.indigo),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -311,7 +311,7 @@ void main() {
Color(PdfColors.indigoAccent700, 'Indigo', 'Accent 700'),
]),
NewPage(),
Header(text: 'Blue'),
Header(text: 'Blue', outlineColor: PdfColors.blue),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -336,7 +336,7 @@ void main() {
Color(PdfColors.blueAccent700, 'Blue', 'Accent 700'),
]),
NewPage(),
Header(text: 'Light Blue'),
Header(text: 'Light Blue', outlineColor: PdfColors.lightBlue),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -365,7 +365,7 @@ void main() {
'Accent 700'),
]),
NewPage(),
Header(text: 'Cyan'),
Header(text: 'Cyan', outlineColor: PdfColors.cyan),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -390,7 +390,7 @@ void main() {
Color(PdfColors.cyanAccent700, 'Cyan', 'Accent 700'),
]),
NewPage(),
Header(text: 'Teal'),
Header(text: 'Teal', outlineColor: PdfColors.teal),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -415,7 +415,7 @@ void main() {
Color(PdfColors.tealAccent700, 'Teal', 'Accent 700'),
]),
NewPage(),
Header(text: 'Green'),
Header(text: 'Green', outlineColor: PdfColors.green),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -440,7 +440,7 @@ void main() {
Color(PdfColors.greenAccent700, 'Green', 'Accent 700'),
]),
NewPage(),
Header(text: 'Light Green'),
Header(text: 'Light Green', outlineColor: PdfColors.lightGreen),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -469,7 +469,7 @@ void main() {
'Accent700'),
]),
NewPage(),
Header(text: 'Lime'),
Header(text: 'Lime', outlineColor: PdfColors.lime),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -494,7 +494,7 @@ void main() {
Color(PdfColors.limeAccent700, 'Lime', 'Accent 700'),
]),
NewPage(),
Header(text: 'Yellow'),
Header(text: 'Yellow', outlineColor: PdfColors.yellow),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -519,7 +519,7 @@ void main() {
Color(PdfColors.yellowAccent700, 'Yellow', 'Accent 700'),
]),
NewPage(),
Header(text: 'Amber'),
Header(text: 'Amber', outlineColor: PdfColors.amber),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -544,7 +544,7 @@ void main() {
Color(PdfColors.amberAccent700, 'Amber', 'Accent 700'),
]),
NewPage(),
Header(text: 'Orange'),
Header(text: 'Orange', outlineColor: PdfColors.orange),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -569,7 +569,7 @@ void main() {
Color(PdfColors.orangeAccent700, 'Orange', 'Accent 700'),
]),
NewPage(),
Header(text: 'Deep Orange'),
Header(text: 'Deep Orange', outlineColor: PdfColors.deepOrange),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -598,7 +598,7 @@ void main() {
'Accent 700'),
]),
NewPage(),
Header(text: 'Brown'),
Header(text: 'Brown', outlineColor: PdfColors.brown),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -619,7 +619,7 @@ void main() {
Color(PdfColors.brown900, 'Brown', '900'),
]),
NewPage(),
Header(text: 'Blue Grey'),
Header(text: 'Blue Grey', outlineColor: PdfColors.blueGrey),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -640,7 +640,7 @@ void main() {
Color(PdfColors.blueGrey900, 'Blue Grey', '900'),
]),
NewPage(),
Header(text: 'Grey'),
Header(text: 'Grey', outlineColor: PdfColors.grey),
GridView(
crossAxisCount: 4,
direction: Axis.vertical,
... ... @@ -677,7 +677,7 @@ void main() {
pdf.addPage(Page(
build: (Context context) => Column(
children: <Widget>[
Header(text: name),
Header(text: name, outlineStyle: PdfOutlineStyle.italic),
SizedBox(
height: context.page.pageFormat.availableWidth,
child: ColorWheel(
... ...
/*
* Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// ignore_for_file: omit_local_variable_types
import 'dart:io';
import 'dart:math';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart';
import 'package:test/test.dart';
Document pdf;
final LoremText lorem = LoremText(random: Random(42));
Iterable<Widget> level(int i) sync* {
final String text = lorem.sentence(5);
int p = 0;
PdfColor color;
PdfOutlineStyle style = PdfOutlineStyle.normal;
if (i >= 3 && i <= 6) {
p++;
}
if (i >= 5 && i <= 6) {
p++;
}
if (i == 15) {
p = 10;
color = PdfColors.amber;
style = PdfOutlineStyle.bold;
}
if (i == 17) {
color = PdfColors.red;
style = PdfOutlineStyle.italic;
}
if (i == 18) {
color = PdfColors.blue;
style = PdfOutlineStyle.italicBold;
}
yield Outline(
child: Text(text),
name: 'anchor$i',
title: text,
level: p,
color: color,
style: style,
);
yield SizedBox(height: 300);
}
void main() {
setUpAll(() {
Document.debug = true;
pdf = Document(pageMode: PdfPageMode.outlines);
});
test('Outline Widget', () {
pdf.addPage(
MultiPage(
build: (Context context) => <Widget>[
for (int i = 0; i < 20; i++) ...level(i),
],
),
);
});
tearDownAll(() {
final File file = File('widgets-outline.pdf');
file.writeAsBytesSync(pdf.save());
});
}
... ...
No preview for this file type
No preview for this file type