main
1import 'dart:math';
2import 'dart:typed_data';
3import 'dart:ui';
4
5import 'package:blood_pressure_app/model/blood_pressure_analyzer.dart';
6import 'package:blood_pressure_app/model/storage/export_columns_store.dart';
7import 'package:blood_pressure_app/model/storage/export_pdf_settings_store.dart';
8import 'package:blood_pressure_app/model/storage/settings_store.dart';
9import 'package:blood_pressure_app/l10n/app_localizations.dart';
10import 'package:health_data_store/health_data_store.dart';
11import 'package:intl/intl.dart';
12import 'package:pdf/pdf.dart';
13import 'package:pdf/widgets.dart' as pw;
14
15/// Utility class for creating pdf files.
16class PdfConverter {
17 /// Create pdf builder.
18 PdfConverter(this.pdfSettings, this.localizations, this.settings, this.availableColumns);
19
20 /// pdf specific settings.
21 final PdfExportSettings pdfSettings;
22
23 /// General customised design information that can be applied to the Pdf.
24 final Settings settings;
25
26 /// Strings in the pdf.
27 final AppLocalizations localizations;
28
29 /// Columns manager used for ex- and import.
30 final ExportColumnsManager availableColumns;
31
32 /// Create a pdf from a record list.
33 Future<Uint8List> create(List<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)> entries) async {
34 final pdf = pw.Document(
35 creator: 'Blood pressure app',
36 );
37 final analyzer = BloodPressureAnalyser(entries.map((e) => e.$2).toList());
38
39 pdf.addPage(pw.MultiPage(
40 pageFormat: PdfPageFormat.a4,
41 build: (pw.Context context) {
42 final title = (pdfSettings.exportTitle) ? _buildPdfTitle(analyzer) : null;
43 title?.layout(context, const pw.BoxConstraints());
44 final statistics = (pdfSettings.exportStatistics) ? _buildPdfStatistics(analyzer) : null;
45 statistics?.layout(context, const pw.BoxConstraints());
46 final availableHeight = PdfPageFormat.a4.availableHeight
47 - (title?.box?.height ?? 0)
48 - (statistics?.box?.height ?? 0);
49 return [
50 if (pdfSettings.exportTitle)
51 title!,
52 if (pdfSettings.exportStatistics)
53 statistics!,
54 if (pdfSettings.exportData)
55 _buildPdfTable(entries, availableHeight),
56 ];
57 },
58 maxPages: 100,
59 ),);
60 return pdf.save();
61 }
62
63 pw.Widget _buildPdfTitle(BloodPressureAnalyser analyzer) {
64 if (analyzer.count < 2) return pw.Text(localizations.errNoData);
65 final dateFormatter = DateFormat(settings.dateFormatString);
66 return pw.Container(
67 child: pw.Text(
68 localizations.pdfDocumentTitle(
69 dateFormatter.format(analyzer.firstDay!),
70 dateFormatter.format(analyzer.lastDay!),
71 ),
72 style: const pw.TextStyle(
73 fontSize: 16,
74 ),
75 ),
76 );
77 }
78
79 pw.Widget _buildPdfStatistics(BloodPressureAnalyser analyzer) => pw.Container(
80 margin: const pw.EdgeInsets.all(20),
81 child: pw.TableHelper.fromTextArray(
82 data: [
83 ['',localizations.sysLong, localizations.diaLong, localizations.pulLong],
84 [localizations.average, analyzer.avgSys?.mmHg, analyzer.avgDia?.mmHg, analyzer.avgPul],
85 [localizations.maximum, analyzer.maxSys?.mmHg, analyzer.maxDia?.mmHg, analyzer.maxPul],
86 [localizations.minimum, analyzer.minSys?.mmHg, analyzer.minDia?.mmHg, analyzer.minPul],
87 ],
88 ),
89 );
90
91 pw.Widget _buildPdfTable(Iterable<(DateTime, BloodPressureRecord, Note, List<MedicineIntake>, Weight?)> entries, double availableHeightOnFirstPage) {
92 final columns = pdfSettings.exportFieldsConfiguration.getActiveColumns(availableColumns);
93 final data = entries.map(
94 (entry) => columns.map(
95 (column) => column.encode(entry.$2, entry.$3, entry.$4, entry.$5),
96 ).toList(),
97 ).toList();
98
99 return pw.Builder(builder: (
100 pw.Context context,) {
101 final realCellHeight = () {
102 final cell = pw.TableHelper.fromTextArray(
103 data: data,
104 border: null,
105 cellHeight: pdfSettings.cellHeight,
106 cellStyle: pw.TextStyle(
107 fontSize: pdfSettings.cellFontSize,
108 ),
109 rowDecoration: const pw.BoxDecoration(
110 border: pw.Border(
111 bottom: pw.BorderSide(
112 color: PdfColors.blueGrey,
113 width: .5,
114 ),
115 ),
116 ),
117 );
118 cell.layout(context, pw.BoxConstraints(maxWidth: PdfPageFormat.a4.availableWidth ~/2 - 10));
119 return cell.box!.height / data.length;
120 }();
121 final realHeaderHeight = () {
122 final cell = pw.TableHelper.fromTextArray(
123 data: [],
124 border: null,
125 headerDecoration: const pw.BoxDecoration(
126 border: pw.Border(bottom: pw.BorderSide()),
127 ),
128 headerHeight: pdfSettings.headerHeight,
129 headerStyle: pw.TextStyle(
130 color: PdfColors.black,
131 fontSize: pdfSettings.headerFontSize,
132 fontWeight: pw.FontWeight.bold,
133 ),
134 headerCellDecoration: pw.BoxDecoration(
135 border: pw.Border(
136 bottom: pw.BorderSide(
137 color: settings.accentColor.toPdfColor(),
138 width: 5,
139 ),
140 ),
141 ),
142 headers: columns.map((c) => c.userTitle(localizations)).toList(),
143 );
144 // subtracting padding during layout
145 cell.layout(context, pw.BoxConstraints(maxWidth: PdfPageFormat.a4.availableWidth ~/2 - 10));
146 return cell.box!.height;
147 }();
148 if (realHeaderHeight > (pdfSettings.headerHeight + 10)
149 || realCellHeight > (pdfSettings.cellHeight + 5)) {
150 return pw.TableHelper.fromTextArray(
151 border: null,
152 cellAlignment: pw.Alignment.centerLeft,
153 headerDecoration: const pw.BoxDecoration(
154 border: pw.Border(bottom: pw.BorderSide()),
155 ),
156 headerHeight: pdfSettings.headerHeight,
157 cellHeight: pdfSettings.cellHeight,
158 cellAlignments: {
159 for (final v in List.generate(columns.length, (idx)=>idx))
160 v : pw.Alignment.centerLeft,
161 },
162 headerStyle: pw.TextStyle(
163 color: PdfColors.black,
164 fontSize: pdfSettings.headerFontSize,
165 fontWeight: pw.FontWeight.bold,
166 ),
167 cellStyle: pw.TextStyle(
168 fontSize: pdfSettings.cellFontSize,
169 ),
170 headerCellDecoration: pw.BoxDecoration(
171 border: pw.Border(
172 bottom: pw.BorderSide(
173 color: settings.accentColor.toPdfColor(),
174 width: 5,
175 ),
176 ),
177 ),
178 rowDecoration: const pw.BoxDecoration(
179 border: pw.Border(
180 bottom: pw.BorderSide(
181 color: PdfColors.blueGrey,
182 width: .5,
183 ),
184 ),
185 ),
186 headers: columns.map((c) => c.userTitle(localizations)).toList(),
187 data: data,
188 );
189 }
190
191 int rowCount = (availableHeightOnFirstPage - realHeaderHeight)
192 ~/ (realCellHeight);
193
194 final List<pw.Widget> tables = [];
195 int pageNum = 0;
196 for (int offset = 0; offset < data.length; offset += (rowCount - 1)) {
197 final dataRange = data.getRange(offset, min(offset + rowCount, data.length)).toList();
198 // Correct rowcount after first page (2 tables)
199 if (pageNum == 1) {
200 rowCount = (PdfPageFormat.a4.availableHeight - realHeaderHeight)
201 ~/ (realCellHeight);
202 offset -= 1;
203 }
204 tables.add(pw.Container(
205 padding: const pw.EdgeInsets.symmetric(horizontal: 5),
206 width: PdfPageFormat.a4.availableWidth / 2 - 1,
207 height: ((pageNum < 2) ? availableHeightOnFirstPage : PdfPageFormat.a4.availableHeight) - 20,
208 alignment: pw.Alignment.topCenter,
209 child: pw.TableHelper.fromTextArray(
210 border: null,
211 cellAlignment: pw.Alignment.centerLeft,
212 headerDecoration: const pw.BoxDecoration(
213 border: pw.Border(bottom: pw.BorderSide()),
214 ),
215 headerHeight: pdfSettings.headerHeight,
216 cellHeight: pdfSettings.cellHeight,
217 cellAlignments: {
218 for (final v in List.generate(columns.length, (idx)=>idx))
219 v : pw.Alignment.centerLeft,
220 },
221 headerStyle: pw.TextStyle(
222 color: PdfColors.black,
223 fontSize: pdfSettings.headerFontSize,
224 fontWeight: pw.FontWeight.bold,
225 ),
226 cellStyle: pw.TextStyle(
227 fontSize: pdfSettings.cellFontSize,
228 ),
229 headerCellDecoration: pw.BoxDecoration(
230 border: pw.Border(
231 bottom: pw.BorderSide(
232 color: settings.accentColor.toPdfColor(),
233 width: 5,
234 ),
235 ),
236 ),
237 rowDecoration: const pw.BoxDecoration(
238 border: pw.Border(
239 bottom: pw.BorderSide(
240 color: PdfColors.blueGrey,
241 width: .5,
242 ),
243 ),
244 ),
245 headers: columns.map((c) => c.userTitle(localizations)).toList(),
246 data: dataRange,
247 ),
248 ),);
249 pageNum++;
250 }
251
252 return pw.Wrap(
253 children: [
254 for (final table in tables)
255 pw.Expanded(child: table),
256 ],
257 );
258 },
259 );
260 }
261}
262
263extension _PdfCompatability on Color {
264 PdfColor toPdfColor() => PdfColor(r / 256, g / 256, b / 256, a);
265}