Showing
3 changed files
with
213 additions
and
75 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, | ||
93 | - this.textAlign = TextAlign.left, | ||
94 | - bool softWrap = true, | ||
95 | - this.textScaleFactor = 1.0, | ||
96 | - int maxLines, | ||
97 | - }) : maxLines = !softWrap ? 1 : maxLines, | ||
98 | - assert(data != null); | 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 | +} | ||
99 | 152 | ||
100 | - final String data; | 153 | +class RichText extends Widget { |
154 | + RichText( | ||
155 | + {@required this.text, | ||
156 | + this.textAlign = TextAlign.left, | ||
157 | + bool softWrap = true, | ||
158 | + this.textScaleFactor = 1.0, | ||
159 | + int maxLines}) | ||
160 | + : maxLines = !softWrap ? 1 : maxLines, | ||
161 | + assert(text != null); | ||
101 | 162 | ||
102 | - TextStyle style; | 163 | + static const bool debug = false; |
164 | + | ||
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 | + } | ||
174 | 236 | ||
175 | - final double ww = box.width; | ||
176 | - final double wh = box.height; | 237 | + final TextStyle style = span.style ?? defaultstyle; |
177 | 238 | ||
178 | - if (x + ww > cw) { | ||
179 | - if (wCount == 0) { | ||
180 | - break; | ||
181 | - } | ||
182 | - w = math.max( | ||
183 | - w, | ||
184 | - _realignLine( | ||
185 | - _words.sublist(lineStart), cw, x - space.width, false)); | ||
186 | - lineStart += wCount; | ||
187 | - if (maxLines != null && ++lines > maxLines) { | ||
188 | - break; | 239 | + final PdfFontMetrics space = |
240 | + style.font.stringMetrics(' ') * (style.fontSize * textScaleFactor); | ||
241 | + | ||
242 | + for (String word in span.text.split(' ')) { | ||
243 | + if (word.isEmpty) { | ||
244 | + offsetX += space.width; | ||
245 | + continue; | ||
189 | } | 246 | } |
190 | 247 | ||
191 | - x = 0.0; | ||
192 | - y += lh + style.lineSpacing; | ||
193 | - h += lh + style.lineSpacing; | ||
194 | - lh = 0.0; | ||
195 | - if (y > ch) { | ||
196 | - break; | 248 | + final PdfFontMetrics metrics = |
249 | + style.font.stringMetrics(word) * (style.fontSize * textScaleFactor); | ||
250 | + | ||
251 | + if (offsetX + metrics.width > constraintWidth) { | ||
252 | + if (wCount == 0) { | ||
253 | + break; | ||
254 | + } | ||
255 | + width = math.max( | ||
256 | + width, | ||
257 | + _realignLine(_words.sublist(lineStart), constraintWidth, | ||
258 | + offsetX - space.width, false, bottom)); | ||
259 | + lineStart += wCount; | ||
260 | + if (maxLines != null && ++lines > maxLines) { | ||
261 | + break; | ||
262 | + } | ||
263 | + | ||
264 | + offsetX = 0.0; | ||
265 | + offsetY += bottom - top + style.lineSpacing; | ||
266 | + top = null; | ||
267 | + bottom = null; | ||
268 | + | ||
269 | + if (offsetY > constraintHeight) { | ||
270 | + return false; | ||
271 | + } | ||
272 | + wCount = 0; | ||
197 | } | 273 | } |
198 | - wCount = 0; | ||
199 | - } | ||
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); | ||
210 | - wCount++; | ||
211 | - } | ||
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)); | 281 | + _words.add(wd); |
282 | + wCount++; | ||
283 | + offsetX += metrics.width + space.advanceWidth; | ||
284 | + } | ||
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