// tool/gen_bench_pdf.dart // // Generates a benchmark PDF at test/assets/large_300p.pdf using // package:syncfusion_flutter_pdf. // // syncfusion_flutter_pdf imports dart:ui which is only available inside the // Flutter SDK runtime, so this file CANNOT be run via plain `dart run`. // // USAGE: // flutter test tool/gen_bench_pdf.dart // flutter test tool/gen_bench_pdf.dart --dart-define=PAGE_COUNT=50 // // The script is structured as a flutter_test file (one `test(...)` block) so // that `flutter test` invokes it with the full Flutter engine (dart:ui present). // It is NOT a real unit test — it is a code-generation tool that happens to // need the Flutter runtime. The test "passes" as long as the file is written // successfully. // // Each page contains: // - A bold title (page number heading) // - Two paragraphs of body text // - A ruled grid of lines (10x10) // - A filled rectangle and an outlined ellipse // This gives a realistic (non-blank) render load for pdfrx frame-timing tests. // ignore_for_file: avoid_print import 'dart:io'; import 'dart:math'; import 'dart:ui' show Offset, Rect, Size; import 'package:flutter_test/flutter_test.dart'; import 'package:syncfusion_flutter_pdf/pdf.dart'; void main() { // Read PAGE_COUNT from --dart-define (default 300). const int pageCount = int.fromEnvironment('PAGE_COUNT', defaultValue: 300); test('generate test/assets/large_300p.pdf ($pageCount pages)', () { final outputPath = _resolveOutputPath(); final outFile = File(outputPath); outFile.parent.createSync(recursive: true); final pdf = PdfDocument(); // Reusable fonts and brushes (created once, shared across pages). final titleFont = PdfStandardFont(PdfFontFamily.helvetica, 18, style: PdfFontStyle.bold); final bodyFont = PdfStandardFont(PdfFontFamily.helvetica, 10); final smallFont = PdfStandardFont(PdfFontFamily.helvetica, 8); final blackBrush = PdfSolidBrush(PdfColor(0, 0, 0)); final darkBlueBrush = PdfSolidBrush(PdfColor(10, 30, 80)); final lightGrayBrush = PdfSolidBrush(PdfColor(220, 220, 220)); final accentBrush = PdfSolidBrush(PdfColor(60, 100, 200)); final gridPen = PdfPen(PdfColor(180, 180, 180), width: 0.3); final borderPen = PdfPen(PdfColor(0, 0, 0), width: 1.0); final accentPen = PdfPen(PdfColor(60, 100, 200), width: 1.5); final rng = Random(42); // deterministic for (int i = 1; i <= pageCount; i++) { final page = pdf.pages.add(); final g = page.graphics; final w = page.getClientSize().width; final h = page.getClientSize().height; // ── Title ────────────────────────────────────────────────────────────── g.drawString( 'BadNote Benchmark — Page $i of $pageCount', titleFont, brush: darkBlueBrush, bounds: Rect.fromLTWH(36, 30, w - 72, 28), ); // Horizontal rule under title g.drawLine( PdfPen(PdfColor(60, 100, 200), width: 1.0), Offset(36, 62), Offset(w - 36, 62), ); // ── Body text (two paragraphs) ──────────────────────────────────────── final paragraph1 = 'This page is part of a synthetic $pageCount-page benchmark PDF ' 'generated by BadNote\'s tool/gen_bench_pdf.dart. Each page carries ' 'non-trivial content (text, vector shapes, a line grid) to simulate ' 'realistic rendering load for pdfrx frame-timing measurements. ' 'Page index: $i. Seed value: ${rng.nextInt(99999)}.'; final paragraph2 = 'Performance target (MUST #4, §10/M1): pdfrx fling-scroll over ' '$pageCount pages in profile mode must stay at ≤ 16.6 ms median ' 'frame time (build + raster) and ≤ 22 ms at p95, measured over ' 'N ≥ 120 frames per §7.1 of the BadNote Phase 1 plan. If this gate ' 'fails the backend choice is invalidated. Fill: ${_lorem(rng, 60)}.'; g.drawString( paragraph1, bodyFont, brush: blackBrush, bounds: Rect.fromLTWH(36, 72, w - 72, 80), format: PdfStringFormat(lineSpacing: 4), ); g.drawString( paragraph2, bodyFont, brush: blackBrush, bounds: Rect.fromLTWH(36, 158, w - 72, 80), format: PdfStringFormat(lineSpacing: 4), ); // ── 10×10 ruled grid ────────────────────────────────────────────────── const gridLeft = 36.0; const gridTop = 260.0; final gridWidth = w - 72; const gridHeight = 220.0; const cols = 10; const rows = 10; final cellW = gridWidth / cols; const cellH = gridHeight / rows; for (int col = 0; col <= cols; col++) { final x = gridLeft + col * cellW; g.drawLine( gridPen, Offset(x, gridTop), Offset(x, gridTop + gridHeight)); } for (int row = 0; row <= rows; row++) { const y = gridTop; g.drawLine(gridPen, Offset(gridLeft, y + row * cellH), Offset(gridLeft + gridWidth, y + row * cellH)); } for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { if ((row + col) % 3 == 0) { g.drawRectangle( brush: lightGrayBrush, bounds: Rect.fromLTWH( gridLeft + col * cellW + 0.5, gridTop + row * cellH + 0.5, cellW - 1, cellH - 1, ), ); } } } g.drawRectangle( pen: borderPen, bounds: Rect.fromLTWH(gridLeft, gridTop, gridWidth, gridHeight), ); for (int row = 0; row < rows; row++) { g.drawString( 'R${row + 1}', smallFont, brush: blackBrush, bounds: Rect.fromLTWH( gridLeft + 2, gridTop + row * cellH + 2, cellW - 4, cellH - 4, ), ); } // ── Accent shapes ────────────────────────────────────────────────────── const shapeTop = gridTop + gridHeight + 18; final rectW = 60.0 + (i % 8) * 10.0; g.drawRectangle( pen: accentPen, brush: accentBrush, bounds: Rect.fromLTWH(36, shapeTop, rectW, 24), ); g.drawString( 'Page $i', smallFont, brush: PdfSolidBrush(PdfColor(255, 255, 255)), bounds: Rect.fromLTWH(40, shapeTop + 6, rectW - 8, 14), ); g.drawEllipse( Rect.fromLTWH(36 + rectW + 16, shapeTop, 80, 24), pen: accentPen, ); // ── Footer ──────────────────────────────────────────────────────────── g.drawString( 'BadNote bench PDF • page $i/$pageCount • tool/gen_bench_pdf.dart', smallFont, brush: PdfSolidBrush(PdfColor(140, 140, 140)), bounds: Rect.fromLTWH(36, h - 28, w - 72, 18), format: PdfStringFormat(alignment: PdfTextAlignment.center), ); } final bytes = pdf.saveSync(); pdf.dispose(); outFile.writeAsBytesSync(bytes); final sizeKb = (outFile.lengthSync() / 1024).toStringAsFixed(1); print('Generated: $outputPath'); print('Pages: $pageCount'); print('Size: ${sizeKb} KB (${outFile.lengthSync()} bytes)'); expect(outFile.existsSync(), isTrue); expect(outFile.lengthSync(), greaterThan(1024), reason: 'PDF must be at least 1 KB'); }, timeout: const Timeout(Duration(minutes: 5))); } /// Resolves test/assets/large_300p.pdf relative to this script's location. /// tool/gen_bench_pdf.dart → project root → test/assets/large_300p.pdf String _resolveOutputPath() { // When run via `flutter test`, the CWD is the project root. return 'test/assets/large_300p.pdf'; } /// Generates a deterministic Lorem-Ipsum-style filler of roughly [words] words. String _lorem(Random rng, int words) { const vocab = [ 'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'eiusmod', 'tempor', 'incididunt', 'labore', 'dolore', 'magna', 'aliqua', 'enim', 'minim', 'veniam', 'quis', 'nostrud', 'exercitation', 'ullamco', 'laboris', 'nisi', 'aliquip', 'commodo', 'consequat', 'duis', 'aute', 'irure', 'reprehenderit', 'voluptate', 'velit', 'esse', 'cillum', 'fugiat', 'nulla', 'pariatur', 'excepteur', 'sint', 'occaecat', 'cupidatat', 'proident', 'culpa', 'officia', 'deserunt', 'mollit', 'anim', ]; return List.generate(words, (_) => vocab[rng.nextInt(vocab.length)]) .join(' '); }