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}