Showing
3 changed files
with
199 additions
and
61 deletions
| @@ -34,14 +34,18 @@ class TextStyle { | @@ -34,14 +34,18 @@ class TextStyle { | ||
| 34 | 34 | ||
| 35 | final PdfFont font; | 35 | final PdfFont font; |
| 36 | 36 | ||
| 37 | + // font height, in pdf unit | ||
| 37 | final double fontSize; | 38 | final double fontSize; |
| 38 | 39 | ||
| 39 | static const double _defaultFontSize = 12.0 * PdfPageFormat.point; | 40 | static const double _defaultFontSize = 12.0 * PdfPageFormat.point; |
| 40 | 41 | ||
| 42 | + // spacing between letters, 1.0 being natural spacing | ||
| 41 | final double letterSpacing; | 43 | final double letterSpacing; |
| 42 | 44 | ||
| 45 | + // spacing between lines, in pdf unit | ||
| 43 | final double lineSpacing; | 46 | final double lineSpacing; |
| 44 | 47 | ||
| 48 | + // spacing between words, 1.0 being natural spacing | ||
| 45 | final double wordSpacing; | 49 | final double wordSpacing; |
| 46 | 50 | ||
| 47 | final double height; | 51 | final double height; |
| @@ -69,37 +73,96 @@ class TextStyle { | @@ -69,37 +73,96 @@ class TextStyle { | ||
| 69 | background: background ?? this.background, | 73 | background: background ?? this.background, |
| 70 | ); | 74 | ); |
| 71 | } | 75 | } |
| 76 | + | ||
| 77 | + @override | ||
| 78 | + String toString() => | ||
| 79 | + 'TextStyle(color:$color font:$font letterSpacing:$letterSpacing wordSpacing:$wordSpacing lineSpacing:$lineSpacing height:$height background:$background)'; | ||
| 72 | } | 80 | } |
| 73 | 81 | ||
| 74 | enum TextAlign { left, right, center, justify } | 82 | enum TextAlign { left, right, center, justify } |
| 75 | 83 | ||
| 76 | class _Word { | 84 | class _Word { |
| 77 | - _Word(this.text, this._box); | 85 | + _Word(this.text, this.style, this.metrics); |
| 78 | 86 | ||
| 79 | final String text; | 87 | final String text; |
| 80 | 88 | ||
| 81 | - PdfRect _box; | 89 | + final TextStyle style; |
| 90 | + | ||
| 91 | + final PdfFontMetrics metrics; | ||
| 92 | + | ||
| 93 | + PdfPoint offset = PdfPoint.zero; | ||
| 82 | 94 | ||
| 83 | @override | 95 | @override |
| 84 | String toString() { | 96 | String toString() { |
| 85 | - return 'Word $text $_box'; | 97 | + return 'Word "$text" offset:$offset metrics:$metrics style:$style'; |
| 98 | + } | ||
| 99 | + | ||
| 100 | + void debugPaint(Context context, double textScaleFactor, PdfRect globalBox) { | ||
| 101 | + const double deb = 5.0; | ||
| 102 | + | ||
| 103 | + context.canvas | ||
| 104 | + ..drawRect(globalBox.x + offset.x + metrics.left, | ||
| 105 | + globalBox.top + offset.y + metrics.top, metrics.width, metrics.height) | ||
| 106 | + ..setStrokeColor(PdfColor.orange) | ||
| 107 | + ..strokePath() | ||
| 108 | + ..drawLine( | ||
| 109 | + globalBox.x + offset.x - deb, | ||
| 110 | + globalBox.top + offset.y, | ||
| 111 | + globalBox.x + offset.x + metrics.right + deb, | ||
| 112 | + globalBox.top + offset.y) | ||
| 113 | + ..setStrokeColor(PdfColor.deepPurple) | ||
| 114 | + ..strokePath(); | ||
| 86 | } | 115 | } |
| 87 | } | 116 | } |
| 88 | 117 | ||
| 89 | -class Text extends Widget { | ||
| 90 | - Text( | ||
| 91 | - this.data, { | ||
| 92 | - this.style, | 118 | +class TextSpan { |
| 119 | + const TextSpan({this.style, this.text, this.children}); | ||
| 120 | + | ||
| 121 | + final TextStyle style; | ||
| 122 | + | ||
| 123 | + final String text; | ||
| 124 | + | ||
| 125 | + final List<TextSpan> children; | ||
| 126 | + | ||
| 127 | + String toPlainText() { | ||
| 128 | + final StringBuffer buffer = StringBuffer(); | ||
| 129 | + visitTextSpan((TextSpan span) { | ||
| 130 | + buffer.write(span.text); | ||
| 131 | + return true; | ||
| 132 | + }); | ||
| 133 | + return buffer.toString(); | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + bool visitTextSpan(bool visitor(TextSpan span)) { | ||
| 137 | + if (text != null) { | ||
| 138 | + if (!visitor(this)) { | ||
| 139 | + return false; | ||
| 140 | + } | ||
| 141 | + } | ||
| 142 | + if (children != null) { | ||
| 143 | + for (TextSpan child in children) { | ||
| 144 | + if (!child.visitTextSpan(visitor)) { | ||
| 145 | + return false; | ||
| 146 | + } | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | + return true; | ||
| 150 | + } | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +class RichText extends Widget { | ||
| 154 | + RichText( | ||
| 155 | + {@required this.text, | ||
| 93 | this.textAlign = TextAlign.left, | 156 | this.textAlign = TextAlign.left, |
| 94 | bool softWrap = true, | 157 | bool softWrap = true, |
| 95 | this.textScaleFactor = 1.0, | 158 | this.textScaleFactor = 1.0, |
| 96 | - int maxLines, | ||
| 97 | - }) : maxLines = !softWrap ? 1 : maxLines, | ||
| 98 | - assert(data != null); | 159 | + int maxLines}) |
| 160 | + : maxLines = !softWrap ? 1 : maxLines, | ||
| 161 | + assert(text != null); | ||
| 99 | 162 | ||
| 100 | - final String data; | 163 | + static const bool debug = false; |
| 101 | 164 | ||
| 102 | - TextStyle style; | 165 | + final TextSpan text; |
| 103 | 166 | ||
| 104 | final TextAlign textAlign; | 167 | final TextAlign textAlign; |
| 105 | 168 | ||
| @@ -109,12 +172,13 @@ class Text extends Widget { | @@ -109,12 +172,13 @@ class Text extends Widget { | ||
| 109 | 172 | ||
| 110 | final List<_Word> _words = <_Word>[]; | 173 | final List<_Word> _words = <_Word>[]; |
| 111 | 174 | ||
| 112 | - double _realignLine( | ||
| 113 | - List<_Word> words, double totalWidth, double wordsWidth, bool last) { | 175 | + double _realignLine(List<_Word> words, double totalWidth, double wordsWidth, |
| 176 | + bool last, double baseline) { | ||
| 114 | double delta = 0.0; | 177 | double delta = 0.0; |
| 115 | switch (textAlign) { | 178 | switch (textAlign) { |
| 116 | case TextAlign.left: | 179 | case TextAlign.left: |
| 117 | - return wordsWidth; | 180 | + totalWidth = wordsWidth; |
| 181 | + break; | ||
| 118 | case TextAlign.right: | 182 | case TextAlign.right: |
| 119 | delta = totalWidth - wordsWidth; | 183 | delta = totalWidth - wordsWidth; |
| 120 | break; | 184 | break; |
| @@ -123,20 +187,20 @@ class Text extends Widget { | @@ -123,20 +187,20 @@ class Text extends Widget { | ||
| 123 | break; | 187 | break; |
| 124 | case TextAlign.justify: | 188 | case TextAlign.justify: |
| 125 | if (last) { | 189 | if (last) { |
| 126 | - return wordsWidth; | 190 | + totalWidth = wordsWidth; |
| 191 | + break; | ||
| 127 | } | 192 | } |
| 128 | delta = (totalWidth - wordsWidth) / (words.length - 1); | 193 | delta = (totalWidth - wordsWidth) / (words.length - 1); |
| 129 | double x = 0.0; | 194 | double x = 0.0; |
| 130 | for (_Word word in words) { | 195 | for (_Word word in words) { |
| 131 | - word._box = PdfRect( | ||
| 132 | - word._box.x + x, word._box.y, word._box.width, word._box.height); | 196 | + word.offset = word.offset.translate(x, -baseline); |
| 133 | x += delta; | 197 | x += delta; |
| 134 | } | 198 | } |
| 135 | return totalWidth; | 199 | return totalWidth; |
| 136 | } | 200 | } |
| 201 | + | ||
| 137 | for (_Word word in words) { | 202 | for (_Word word in words) { |
| 138 | - word._box = PdfRect( | ||
| 139 | - word._box.x + delta, word._box.y, word._box.width, word._box.height); | 203 | + word.offset = word.offset.translate(delta, -baseline); |
| 140 | } | 204 | } |
| 141 | return totalWidth; | 205 | return totalWidth; |
| 142 | } | 206 | } |
| @@ -146,74 +210,89 @@ class Text extends Widget { | @@ -146,74 +210,89 @@ class Text extends Widget { | ||
| 146 | {bool parentUsesSize = false}) { | 210 | {bool parentUsesSize = false}) { |
| 147 | _words.clear(); | 211 | _words.clear(); |
| 148 | 212 | ||
| 149 | - style ??= Theme.of(context).defaultTextStyle; | 213 | + final TextStyle defaultstyle = Theme.of(context).defaultTextStyle; |
| 150 | 214 | ||
| 151 | - final double cw = constraints.hasBoundedWidth | 215 | + final double constraintWidth = constraints.hasBoundedWidth |
| 152 | ? constraints.maxWidth | 216 | ? constraints.maxWidth |
| 153 | : constraints.constrainWidth(); | 217 | : constraints.constrainWidth(); |
| 154 | - final double ch = constraints.hasBoundedHeight | 218 | + final double constraintHeight = constraints.hasBoundedHeight |
| 155 | ? constraints.maxHeight | 219 | ? constraints.maxHeight |
| 156 | : constraints.constrainHeight(); | 220 | : constraints.constrainHeight(); |
| 157 | 221 | ||
| 158 | - double x = 0.0; | ||
| 159 | - double y = 0.0; | ||
| 160 | - double w = 0.0; | ||
| 161 | - double h = 0.0; | ||
| 162 | - double lh = 0.0; | ||
| 163 | - | ||
| 164 | - final PdfRect space = | ||
| 165 | - style.font.stringBounds(' ') * (style.fontSize * textScaleFactor); | 222 | + double offsetX = 0.0; |
| 223 | + double offsetY = 0.0; | ||
| 224 | + double width = 0.0; | ||
| 225 | + double top; | ||
| 226 | + double bottom; | ||
| 166 | 227 | ||
| 167 | int lines = 1; | 228 | int lines = 1; |
| 168 | int wCount = 0; | 229 | int wCount = 0; |
| 169 | int lineStart = 0; | 230 | int lineStart = 0; |
| 170 | 231 | ||
| 171 | - for (String word in data.split(' ')) { | ||
| 172 | - final PdfRect box = | ||
| 173 | - style.font.stringBounds(word) * (style.fontSize * textScaleFactor); | 232 | + text.visitTextSpan((TextSpan span) { |
| 233 | + if (span.text == null) { | ||
| 234 | + return true; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + final TextStyle style = span.style ?? defaultstyle; | ||
| 174 | 238 | ||
| 175 | - final double ww = box.width; | ||
| 176 | - final double wh = box.height; | 239 | + final PdfFontMetrics space = |
| 240 | + style.font.stringMetrics(' ') * (style.fontSize * textScaleFactor); | ||
| 177 | 241 | ||
| 178 | - if (x + ww > cw) { | 242 | + for (String word in span.text.split(' ')) { |
| 243 | + if (word.isEmpty) { | ||
| 244 | + offsetX += space.width; | ||
| 245 | + continue; | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + final PdfFontMetrics metrics = | ||
| 249 | + style.font.stringMetrics(word) * (style.fontSize * textScaleFactor); | ||
| 250 | + | ||
| 251 | + if (offsetX + metrics.width > constraintWidth) { | ||
| 179 | if (wCount == 0) { | 252 | if (wCount == 0) { |
| 180 | break; | 253 | break; |
| 181 | } | 254 | } |
| 182 | - w = math.max( | ||
| 183 | - w, | ||
| 184 | - _realignLine( | ||
| 185 | - _words.sublist(lineStart), cw, x - space.width, false)); | 255 | + width = math.max( |
| 256 | + width, | ||
| 257 | + _realignLine(_words.sublist(lineStart), constraintWidth, | ||
| 258 | + offsetX - space.width, false, bottom)); | ||
| 186 | lineStart += wCount; | 259 | lineStart += wCount; |
| 187 | if (maxLines != null && ++lines > maxLines) { | 260 | if (maxLines != null && ++lines > maxLines) { |
| 188 | break; | 261 | break; |
| 189 | } | 262 | } |
| 190 | 263 | ||
| 191 | - x = 0.0; | ||
| 192 | - y += lh + style.lineSpacing; | ||
| 193 | - h += lh + style.lineSpacing; | ||
| 194 | - lh = 0.0; | ||
| 195 | - if (y > ch) { | ||
| 196 | - break; | 264 | + offsetX = 0.0; |
| 265 | + offsetY += bottom - top + style.lineSpacing; | ||
| 266 | + top = null; | ||
| 267 | + bottom = null; | ||
| 268 | + | ||
| 269 | + if (offsetY > constraintHeight) { | ||
| 270 | + return false; | ||
| 197 | } | 271 | } |
| 198 | wCount = 0; | 272 | wCount = 0; |
| 199 | } | 273 | } |
| 200 | 274 | ||
| 201 | - final double wx = x; | ||
| 202 | - final double wy = y; | 275 | + top = math.min(top ?? metrics.top, metrics.top); |
| 276 | + bottom = math.max(bottom ?? metrics.bottom, metrics.bottom); | ||
| 203 | 277 | ||
| 204 | - x += ww + space.width; | ||
| 205 | - lh = math.max(lh, wh); | 278 | + final _Word wd = _Word(word, style, metrics); |
| 279 | + wd.offset = PdfPoint(offsetX, -offsetY); | ||
| 206 | 280 | ||
| 207 | - final _Word wd = | ||
| 208 | - _Word(word, PdfRect(box.x + wx, box.y + wy + wh, ww, wh)); | ||
| 209 | _words.add(wd); | 281 | _words.add(wd); |
| 210 | wCount++; | 282 | wCount++; |
| 283 | + offsetX += metrics.width + space.advanceWidth; | ||
| 211 | } | 284 | } |
| 212 | - w = math.max( | ||
| 213 | - w, _realignLine(_words.sublist(lineStart), cw, x - space.width, true)); | ||
| 214 | - h += lh; | ||
| 215 | - box = PdfRect(0.0, 0.0, constraints.constrainWidth(w), | ||
| 216 | - constraints.constrainHeight(h)); | 285 | + |
| 286 | + offsetX -= space.width; | ||
| 287 | + return true; | ||
| 288 | + }); | ||
| 289 | + | ||
| 290 | + width = math.max( | ||
| 291 | + width, | ||
| 292 | + _realignLine( | ||
| 293 | + _words.sublist(lineStart), constraintWidth, offsetX, true, bottom)); | ||
| 294 | + box = PdfRect(0.0, 0.0, constraints.constrainWidth(width), | ||
| 295 | + constraints.constrainHeight(offsetY + bottom - top)); | ||
| 217 | } | 296 | } |
| 218 | 297 | ||
| 219 | @override | 298 | @override |
| @@ -227,11 +306,48 @@ class Text extends Widget { | @@ -227,11 +306,48 @@ class Text extends Widget { | ||
| 227 | @override | 306 | @override |
| 228 | void paint(Context context) { | 307 | void paint(Context context) { |
| 229 | super.paint(context); | 308 | super.paint(context); |
| 230 | - context.canvas.setFillColor(style.color); | 309 | + TextStyle currentStyle; |
| 310 | + PdfColor currentColor; | ||
| 231 | 311 | ||
| 232 | for (_Word word in _words) { | 312 | for (_Word word in _words) { |
| 233 | - context.canvas.drawString(style.font, style.fontSize * textScaleFactor, | ||
| 234 | - word.text, box.x + word._box.x, box.y + box.height - word._box.y); | 313 | + assert(() { |
| 314 | + if (Document.debug && RichText.debug) { | ||
| 315 | + word.debugPaint(context, textScaleFactor, box); | ||
| 316 | + } | ||
| 317 | + return true; | ||
| 318 | + }()); | ||
| 319 | + | ||
| 320 | + if (word.style != currentStyle) { | ||
| 321 | + currentStyle = word.style; | ||
| 322 | + if (currentStyle.color != currentColor) { | ||
| 323 | + currentColor = currentStyle.color; | ||
| 324 | + context.canvas.setFillColor(currentColor); | ||
| 325 | + } | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + context.canvas.drawString( | ||
| 329 | + currentStyle.font, | ||
| 330 | + currentStyle.fontSize * textScaleFactor, | ||
| 331 | + word.text, | ||
| 332 | + box.x + word.offset.x, | ||
| 333 | + box.top + word.offset.y); | ||
| 235 | } | 334 | } |
| 236 | } | 335 | } |
| 237 | } | 336 | } |
| 337 | + | ||
| 338 | +class Text extends RichText { | ||
| 339 | + Text( | ||
| 340 | + String text, { | ||
| 341 | + TextStyle style, | ||
| 342 | + TextAlign textAlign = TextAlign.left, | ||
| 343 | + bool softWrap = true, | ||
| 344 | + double textScaleFactor = 1.0, | ||
| 345 | + int maxLines, | ||
| 346 | + }) : assert(text != null), | ||
| 347 | + super( | ||
| 348 | + text: TextSpan(text: text, style: style), | ||
| 349 | + textAlign: textAlign, | ||
| 350 | + softWrap: softWrap, | ||
| 351 | + textScaleFactor: textScaleFactor, | ||
| 352 | + maxLines: maxLines); | ||
| 353 | +} |
| @@ -16,6 +16,7 @@ | @@ -16,6 +16,7 @@ | ||
| 16 | 16 | ||
| 17 | import 'dart:convert'; | 17 | import 'dart:convert'; |
| 18 | import 'dart:io'; | 18 | import 'dart:io'; |
| 19 | +import 'dart:typed_data'; | ||
| 19 | 20 | ||
| 20 | import 'package:pdf/pdf.dart'; | 21 | import 'package:pdf/pdf.dart'; |
| 21 | import 'package:pdf/widgets.dart'; | 22 | import 'package:pdf/widgets.dart'; |
| @@ -97,6 +98,10 @@ void main() { | @@ -97,6 +98,10 @@ void main() { | ||
| 97 | children: List<Widget>.generate( | 98 | children: List<Widget>.generate( |
| 98 | 9, (int n) => FittedBox(child: Text('${n + 1}'))))))); | 99 | 9, (int n) => FittedBox(child: Text('${n + 1}'))))))); |
| 99 | 100 | ||
| 101 | + final Uint8List robotoData = File('open-sans.ttf').readAsBytesSync(); | ||
| 102 | + final PdfTtfFont roboto = | ||
| 103 | + PdfTtfFont(pdf.document, robotoData.buffer.asByteData()); | ||
| 104 | + | ||
| 100 | pdf.addPage(MultiPage( | 105 | pdf.addPage(MultiPage( |
| 101 | pageFormat: const PdfPageFormat(400.0, 200.0), | 106 | pageFormat: const PdfPageFormat(400.0, 200.0), |
| 102 | margin: const EdgeInsets.all(10.0), | 107 | margin: const EdgeInsets.all(10.0), |
| @@ -141,6 +146,22 @@ void main() { | @@ -141,6 +146,22 @@ void main() { | ||
| 141 | ..drawRRect(0, 0, size.x, size.y, 10, 10) | 146 | ..drawRRect(0, 0, size.x, size.y, 10, 10) |
| 142 | ..fillPath(); | 147 | ..fillPath(); |
| 143 | }), | 148 | }), |
| 149 | + RichText( | ||
| 150 | + text: TextSpan( | ||
| 151 | + text: 'Hello ', | ||
| 152 | + style: Theme.of(context).defaultTextStyle, | ||
| 153 | + children: <TextSpan>[ | ||
| 154 | + TextSpan( | ||
| 155 | + text: 'bold', | ||
| 156 | + style: Theme.of(context) | ||
| 157 | + .defaultTextStyleBold | ||
| 158 | + .copyWith(fontSize: 20, color: PdfColor.blue)), | ||
| 159 | + const TextSpan( | ||
| 160 | + text: ' world!', | ||
| 161 | + ), | ||
| 162 | + ], | ||
| 163 | + ), | ||
| 164 | + ) | ||
| 144 | ])); | 165 | ])); |
| 145 | 166 | ||
| 146 | final File file = File('widgets.pdf'); | 167 | final File file = File('widgets.pdf'); |
-
Please register or login to post a comment