David PHAM-VAN

Implement PdfPageLabels

@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 4
5 - Update xml dependency range 5 - Update xml dependency range
6 - Implement PointDataSet for Chart 6 - Implement PointDataSet for Chart
  7 +- Implement PdfPageLabels
7 8
8 ## 3.7.4 9 ## 3.7.4
9 10
@@ -33,6 +33,7 @@ export 'src/pdf/obj/info.dart'; @@ -33,6 +33,7 @@ export 'src/pdf/obj/info.dart';
33 export 'src/pdf/obj/metadata.dart'; 33 export 'src/pdf/obj/metadata.dart';
34 export 'src/pdf/obj/outline.dart'; 34 export 'src/pdf/obj/outline.dart';
35 export 'src/pdf/obj/page.dart'; 35 export 'src/pdf/obj/page.dart';
  36 +export 'src/pdf/obj/page_label.dart';
36 export 'src/pdf/obj/pattern.dart'; 37 export 'src/pdf/obj/pattern.dart';
37 export 'src/pdf/obj/shading.dart'; 38 export 'src/pdf/obj/shading.dart';
38 export 'src/pdf/obj/signature.dart'; 39 export 'src/pdf/obj/signature.dart';
@@ -30,6 +30,7 @@ import 'obj/names.dart'; @@ -30,6 +30,7 @@ import 'obj/names.dart';
30 import 'obj/object.dart'; 30 import 'obj/object.dart';
31 import 'obj/outline.dart'; 31 import 'obj/outline.dart';
32 import 'obj/page.dart'; 32 import 'obj/page.dart';
  33 +import 'obj/page_label.dart';
33 import 'obj/page_list.dart'; 34 import 'obj/page_list.dart';
34 import 'obj/signature.dart'; 35 import 'obj/signature.dart';
35 import 'output.dart'; 36 import 'output.dart';
@@ -83,10 +84,8 @@ class PdfDocument { @@ -83,10 +84,8 @@ class PdfDocument {
83 }) : deflate = compress ? (deflate ?? defaultDeflate) : null, 84 }) : deflate = compress ? (deflate ?? defaultDeflate) : null,
84 prev = null, 85 prev = null,
85 _objser = 1 { 86 _objser = 1 {
86 - // Now create some standard objects  
87 - pdfPageList = PdfPageList(this);  
88 - pdfNames = PdfNames(this);  
89 - catalog = PdfCatalog(this, pdfPageList, pageMode, pdfNames); 87 + // create the catalog
  88 + catalog = PdfCatalog(this, PdfPageList(this), pageMode);
90 } 89 }
91 90
92 PdfDocument.load( 91 PdfDocument.load(
@@ -99,9 +98,7 @@ class PdfDocument { @@ -99,9 +98,7 @@ class PdfDocument {
99 _objser = prev!.size, 98 _objser = prev!.size,
100 version = prev.version { 99 version = prev.version {
101 // Now create some standard objects 100 // Now create some standard objects
102 - pdfPageList = PdfPageList(this);  
103 - pdfNames = PdfNames(this);  
104 - catalog = PdfCatalog(this, pdfPageList, pageMode, pdfNames); 101 + catalog = PdfCatalog(this, PdfPageList(this), pageMode);
105 102
106 // Import the existing document 103 // Import the existing document
107 prev!.mergeDocument(this); 104 prev!.mergeDocument(this);
@@ -118,7 +115,7 @@ class PdfDocument { @@ -118,7 +115,7 @@ class PdfDocument {
118 final Set<PdfObject> objects = <PdfObject>{}; 115 final Set<PdfObject> objects = <PdfObject>{};
119 116
120 /// This is the Catalog object, which is required by each Pdf Document 117 /// This is the Catalog object, which is required by each Pdf Document
121 - late PdfCatalog catalog; 118 + late final PdfCatalog catalog;
122 119
123 /// PDF version to generate 120 /// PDF version to generate
124 final PdfVersion version; 121 final PdfVersion version;
@@ -129,13 +126,13 @@ class PdfDocument { @@ -129,13 +126,13 @@ class PdfDocument {
129 PdfInfo? info; 126 PdfInfo? info;
130 127
131 /// This is the Pages object, which is required by each Pdf Document 128 /// This is the Pages object, which is required by each Pdf Document
132 - late PdfPageList pdfPageList;  
133 -  
134 - /// The name dictionary  
135 - late PdfNames pdfNames; 129 + PdfPageList get pdfPageList => catalog.pdfPageList;
136 130
137 - /// This is the Outline object, which is optional  
138 - PdfOutline? _outline; 131 + /// The anchor names dictionary
  132 + PdfNames get pdfNames {
  133 + catalog.names ??= PdfNames(this);
  134 + return catalog.names!;
  135 + }
139 136
140 /// This holds a [PdfObject] describing the default border for annotations. 137 /// This holds a [PdfObject] describing the default border for annotations.
141 /// It's only used when the document is being written. 138 /// It's only used when the document is being written.
@@ -191,11 +188,14 @@ class PdfDocument { @@ -191,11 +188,14 @@ class PdfDocument {
191 188
192 /// The root outline 189 /// The root outline
193 PdfOutline get outline { 190 PdfOutline get outline {
194 - if (_outline == null) {  
195 - _outline = PdfOutline(this);  
196 - catalog.outlines = _outline;  
197 - }  
198 - return _outline!; 191 + catalog.outlines ??= PdfOutline(this);
  192 + return catalog.outlines!;
  193 + }
  194 +
  195 + /// The root page labels
  196 + PdfPageLabels get pageLabels {
  197 + catalog.pageLabels ??= PdfPageLabels(this);
  198 + return catalog.pageLabels!;
199 } 199 }
200 200
201 /// Graphic states for opacity and transfer modes 201 /// Graphic states for opacity and transfer modes
@@ -21,6 +21,7 @@ import 'metadata.dart'; @@ -21,6 +21,7 @@ import 'metadata.dart';
21 import 'names.dart'; 21 import 'names.dart';
22 import 'object_dict.dart'; 22 import 'object_dict.dart';
23 import 'outline.dart'; 23 import 'outline.dart';
  24 +import 'page_label.dart';
24 import 'page_list.dart'; 25 import 'page_list.dart';
25 26
26 /// Pdf Catalog object 27 /// Pdf Catalog object
@@ -30,7 +31,6 @@ class PdfCatalog extends PdfObjectDict { @@ -30,7 +31,6 @@ class PdfCatalog extends PdfObjectDict {
30 PdfDocument pdfDocument, 31 PdfDocument pdfDocument,
31 this.pdfPageList, 32 this.pdfPageList,
32 this.pageMode, 33 this.pageMode,
33 - this.names,  
34 ) : super(pdfDocument, type: '/Catalog'); 34 ) : super(pdfDocument, type: '/Catalog');
35 35
36 /// The pages of the document 36 /// The pages of the document
@@ -45,8 +45,11 @@ class PdfCatalog extends PdfObjectDict { @@ -45,8 +45,11 @@ class PdfCatalog extends PdfObjectDict {
45 /// The initial page mode 45 /// The initial page mode
46 final PdfPageMode pageMode; 46 final PdfPageMode pageMode;
47 47
48 - /// The initial page mode  
49 - final PdfNames names; 48 + /// The anchor names
  49 + PdfNames? names;
  50 +
  51 + /// The page labels of the document
  52 + PdfPageLabels? pageLabels;
50 53
51 /// These map the page modes just defined to the pagemodes setting of Pdf. 54 /// These map the page modes just defined to the pagemodes setting of Pdf.
52 static const List<String> _pdfPageModes = <String>[ 55 static const List<String> _pdfPageModes = <String>[
@@ -75,7 +78,14 @@ class PdfCatalog extends PdfObjectDict { @@ -75,7 +78,14 @@ class PdfCatalog extends PdfObjectDict {
75 } 78 }
76 79
77 // the Names object 80 // the Names object
78 - params['/Names'] = names.ref(); 81 + if (names != null) {
  82 + params['/Names'] = names!.ref();
  83 + }
  84 +
  85 + // the PageLabels object
  86 + if (pageLabels != null && pageLabels!.labels.isNotEmpty) {
  87 + params['/PageLabels'] = pageLabels!.ref();
  88 + }
79 89
80 // the /PageMode setting 90 // the /PageMode setting
81 params['/PageMode'] = PdfName(_pdfPageModes[pageMode.index]); 91 params['/PageMode'] = PdfName(_pdfPageModes[pageMode.index]);
@@ -58,7 +58,7 @@ class PdfOutline extends PdfObjectDict { @@ -58,7 +58,7 @@ class PdfOutline extends PdfObjectDict {
58 this.color, 58 this.color,
59 this.destMode = PdfOutlineMode.fitPage, 59 this.destMode = PdfOutlineMode.fitPage,
60 this.style = PdfOutlineStyle.normal, 60 this.style = PdfOutlineStyle.normal,
61 - int? page, 61 + String? page,
62 }) : assert(anchor == null || (dest == null && rect == null)), 62 }) : assert(anchor == null || (dest == null && rect == null)),
63 _page = page, 63 _page = page,
64 super(pdfDocument); 64 super(pdfDocument);
@@ -76,10 +76,12 @@ class PdfOutline extends PdfObjectDict { @@ -76,10 +76,12 @@ class PdfOutline extends PdfObjectDict {
76 PdfPage? dest; 76 PdfPage? dest;
77 77
78 /// Page number 78 /// Page number
79 - int? get page => 79 + String? get page =>
80 _page ?? 80 _page ??
81 - (dest != null ? pdfDocument.pdfPageList.pages.indexOf(dest!) + 1 : null);  
82 - final int? _page; 81 + (dest != null
  82 + ? (pdfDocument.pdfPageList.pages.indexOf(dest!) + 1).toString()
  83 + : null);
  84 + final String? _page;
83 85
84 /// The region on the destination page 86 /// The region on the destination page
85 final PdfRect? rect; 87 final PdfRect? rect;
  1 +import '../../priv.dart';
  2 +import '../document.dart';
  3 +import 'object_dict.dart';
  4 +
  5 +enum PdfPageLabelStyle {
  6 + arabic,
  7 + romanUpper,
  8 + romanLower,
  9 + lettersUpper,
  10 + lettersLower
  11 +}
  12 +
  13 +class PdfPageLabel {
  14 + PdfPageLabel(this.prefix, {this.style, this.subsequent});
  15 +
  16 + PdfPageLabel.arabic({this.prefix, this.subsequent})
  17 + : style = PdfPageLabelStyle.arabic;
  18 +
  19 + PdfPageLabel.romanUpper({this.prefix, this.subsequent})
  20 + : style = PdfPageLabelStyle.romanUpper;
  21 +
  22 + PdfPageLabel.romanLower({this.prefix, this.subsequent})
  23 + : style = PdfPageLabelStyle.romanLower;
  24 +
  25 + PdfPageLabel.lettersUpper({this.prefix, this.subsequent})
  26 + : style = PdfPageLabelStyle.lettersUpper;
  27 +
  28 + PdfPageLabel.lettersLower({this.prefix, this.subsequent})
  29 + : style = PdfPageLabelStyle.lettersLower;
  30 +
  31 + final PdfPageLabelStyle? style;
  32 + final String? prefix;
  33 + final int? subsequent;
  34 +
  35 + PdfDict toDict(PdfObject obj) {
  36 + final PdfName? s;
  37 + switch (style) {
  38 + case PdfPageLabelStyle.arabic:
  39 + s = const PdfName('/D');
  40 + break;
  41 + case PdfPageLabelStyle.romanUpper:
  42 + s = const PdfName('/R');
  43 + break;
  44 + case PdfPageLabelStyle.romanLower:
  45 + s = const PdfName('/r');
  46 + break;
  47 + case PdfPageLabelStyle.lettersUpper:
  48 + s = const PdfName('/A');
  49 + break;
  50 + case PdfPageLabelStyle.lettersLower:
  51 + s = const PdfName('/a');
  52 + break;
  53 + case null:
  54 + s = null;
  55 + }
  56 + return PdfDict({
  57 + if (s != null) '/S': s,
  58 + if (prefix != null && prefix!.isNotEmpty)
  59 + '/P': PdfSecString.fromString(obj, prefix!),
  60 + if (subsequent != null) '/St': PdfNum(subsequent!)
  61 + });
  62 + }
  63 +
  64 + String _toRoman(int decimal) {
  65 + const dictionary = {
  66 + 1000: 'M',
  67 + 900: 'CM',
  68 + 500: 'D',
  69 + 400: 'CD,',
  70 + 100: 'C',
  71 + 90: 'XC',
  72 + 50: 'L',
  73 + 40: 'XL',
  74 + 10: 'X',
  75 + 9: 'IX',
  76 + 5: 'V',
  77 + 4: 'IV',
  78 + 1: 'I'
  79 + };
  80 +
  81 + assert(decimal > 0 && decimal < 3999,
  82 + 'Roman numerals are limited to the inclusive range of 1 to 3999.');
  83 +
  84 + var result = '';
  85 + dictionary.forEach((k, v) {
  86 + while (decimal >= k) {
  87 + decimal -= k;
  88 + result += v;
  89 + }
  90 + });
  91 + return result;
  92 + }
  93 +
  94 + String _toLetters(int decimal) {
  95 + final n = String.fromCharCode(0x41 + decimal % 26);
  96 + final r = decimal ~/ 26 + 1;
  97 + return n * r;
  98 + }
  99 +
  100 + String asString([int index = 0]) {
  101 + final i = subsequent == null ? index : index + subsequent!;
  102 +
  103 + final String suffix;
  104 + switch (style) {
  105 + case PdfPageLabelStyle.arabic:
  106 + suffix = (i + 1).toString();
  107 + break;
  108 + case PdfPageLabelStyle.romanUpper:
  109 + suffix = _toRoman(i + 1);
  110 + break;
  111 + case PdfPageLabelStyle.romanLower:
  112 + suffix = _toRoman(i + 1).toLowerCase();
  113 + break;
  114 + case PdfPageLabelStyle.lettersUpper:
  115 + suffix = _toLetters(i);
  116 + break;
  117 + case PdfPageLabelStyle.lettersLower:
  118 + suffix = _toLetters(i).toLowerCase();
  119 + break;
  120 + case null:
  121 + suffix = '';
  122 + }
  123 + return '${prefix ?? ''}$suffix';
  124 + }
  125 +}
  126 +
  127 +/// Pdf PageLabels object
  128 +class PdfPageLabels extends PdfObjectDict {
  129 + /// Constructs a Pdf PageLabels object.
  130 + PdfPageLabels(PdfDocument pdfDocument) : super(pdfDocument);
  131 +
  132 + final labels = <int, PdfPageLabel>{};
  133 +
  134 + String pageLabel(int index) {
  135 + final n = labels.keys.toList()..sort();
  136 + var current = PdfPageLabel.arabic();
  137 + var s = 0;
  138 + for (final i in n) {
  139 + if (index >= i) {
  140 + current = labels[i]!;
  141 + s = i;
  142 + }
  143 + }
  144 +
  145 + return current.asString(index - s);
  146 + }
  147 +
  148 + Iterable<String> get names sync* {
  149 + final n = labels.keys.toList()..sort();
  150 + var l = PdfPageLabel.arabic();
  151 + final len = pdfDocument.pdfPageList.pages.length;
  152 + var c = 0;
  153 + var b = c < n.length ? n[c] : len;
  154 + var s = 0;
  155 + for (var i = 0; i < len; i++) {
  156 + if (i >= b) {
  157 + l = labels[b]!;
  158 + c++;
  159 + b = c < n.length ? n[c] : len;
  160 + s = i;
  161 + }
  162 + yield l.asString(i - s);
  163 + }
  164 + }
  165 +
  166 + @override
  167 + void prepare() {
  168 + super.prepare();
  169 +
  170 + final nums = PdfArray();
  171 + for (final entry in labels.entries) {
  172 + nums.add(PdfNum(entry.key));
  173 + nums.add(entry.value.toDict(this));
  174 + }
  175 +
  176 + params['/Nums'] = nums;
  177 + }
  178 +}
@@ -622,7 +622,7 @@ class Outline extends Anchor { @@ -622,7 +622,7 @@ class Outline extends Anchor {
622 anchor: name, 622 anchor: name,
623 color: color, 623 color: color,
624 style: style, 624 style: style,
625 - page: context.pageNumber, 625 + page: context.pageLabel,
626 )..effectiveLevel = level; 626 )..effectiveLevel = level;
627 627
628 final root = context.document.outline; 628 final root = context.document.outline;
@@ -62,7 +62,16 @@ class Context { @@ -62,7 +62,16 @@ class Context {
62 62
63 final PdfDocument document; 63 final PdfDocument document;
64 64
65 - int get pageNumber => document.pdfPageList.pages.indexOf(page) + 1; 65 + int get _pageNumber => document.pdfPageList.pages.indexOf(page);
  66 +
  67 + int get pageNumber => _pageNumber + 1;
  68 +
  69 + String get pageLabel => document.catalog.pageLabels == null
  70 + ? pageNumber.toString()
  71 + : document.pageLabels.pageLabel(_pageNumber);
  72 +
  73 + set pageLabel(String value) =>
  74 + document.pageLabels.labels[_pageNumber] = PdfPageLabel(value);
66 75
67 /// Number of pages in the document. 76 /// Number of pages in the document.
68 /// This value is not available in a MultiPage body and will be equal to pageNumber. 77 /// This value is not available in a MultiPage body and will be equal to pageNumber.
  1 +/*
  2 + * Copyright (C) 2017, David PHAM-VAN <dev.nfet.net@gmail.com>
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import 'package:pdf/pdf.dart';
  18 +import 'package:test/test.dart';
  19 +
  20 +void main() {
  21 + test('Page label', () async {
  22 + final pdf = PdfDocument();
  23 +
  24 + for (var i = 0; i < 5; i++) {
  25 + PdfPage(pdf);
  26 + }
  27 +
  28 + pdf.pageLabels.labels[0] = PdfPageLabel('Hello');
  29 + pdf.pageLabels.labels[1] = PdfPageLabel.lettersUpper();
  30 + pdf.pageLabels.labels[3] = PdfPageLabel.romanLower(prefix: 'appendix ');
  31 +
  32 + expect(pdf.pageLabels.pageLabel(0), 'Hello');
  33 + expect(pdf.pageLabels.pageLabel(1), 'A');
  34 + expect(pdf.pageLabels.pageLabel(2), 'B');
  35 + expect(pdf.pageLabels.pageLabel(3), 'appendix i');
  36 + expect(pdf.pageLabels.pageLabel(4), 'appendix ii');
  37 + });
  38 +
  39 + test('Roman labels', () async {
  40 + final pdf = PdfDocument();
  41 +
  42 + for (var i = 0; i < 500; i++) {
  43 + PdfPage(pdf);
  44 + }
  45 +
  46 + pdf.pageLabels.labels[3] = PdfPageLabel.romanLower();
  47 +
  48 + expect(pdf.pageLabels.pageLabel(0), '1');
  49 + expect(pdf.pageLabels.pageLabel(300), 'ccxcviii');
  50 + expect(pdf.pageLabels.names.toList()[300], 'ccxcviii');
  51 + });
  52 +
  53 + test('No of labels', () async {
  54 + final pdf = PdfDocument();
  55 +
  56 + for (var i = 0; i < 500; i++) {
  57 + PdfPage(pdf);
  58 + }
  59 +
  60 + expect(pdf.pageLabels.pageLabel(0), '1');
  61 + expect(pdf.pageLabels.pageLabel(300), '301');
  62 + expect(pdf.pageLabels.names.toList()[300], '301');
  63 + });
  64 +
  65 + test('Letter labels', () async {
  66 + final pdf = PdfDocument();
  67 +
  68 + for (var i = 0; i < 500; i++) {
  69 + PdfPage(pdf);
  70 + }
  71 +
  72 + pdf.pageLabels.labels[30] = PdfPageLabel.lettersLower();
  73 +
  74 + expect(pdf.pageLabels.pageLabel(0), '1');
  75 + expect(pdf.pageLabels.pageLabel(30), 'a');
  76 + expect(pdf.pageLabels.names.toList()[30], 'a');
  77 + expect(pdf.pageLabels.pageLabel(40), 'k');
  78 + expect(pdf.pageLabels.names.toList()[40], 'k');
  79 + expect(pdf.pageLabels.pageLabel(60), 'ee');
  80 + expect(pdf.pageLabels.names.toList()[60], 'ee');
  81 + });
  82 +}