-
Notifications
You must be signed in to change notification settings - Fork 89
Expand file tree
/
Copy pathText.java
More file actions
342 lines (289 loc) · 10 KB
/
Text.java
File metadata and controls
342 lines (289 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package nodebox.graphics;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import java.util.Hashtable;
import java.util.Iterator;
public class Text extends AbstractGrob {
public enum Align {
LEFT, RIGHT, CENTER, JUSTIFY
}
private String text;
private double baseLineX, baseLineY;
private double width = 0;
private double height = 0;
private String fontName = "Helvetica";
private double fontSize = 24;
private double lineHeight = 1.2;
private Align align = Align.CENTER;
private Color fillColor = new Color();
public Text(String text, Point pt) {
this.text = text;
this.baseLineX = pt.getX();
this.baseLineY = pt.getY();
}
public Text(String text, double baseLineX, double baseLineY) {
this.text = text;
this.baseLineX = baseLineX;
this.baseLineY = baseLineY;
}
public Text(String text, Rect r) {
this.text = text;
this.baseLineX = r.getX();
this.baseLineY = r.getY();
this.width = r.getWidth();
this.height = r.getHeight();
}
public Text(String text, double x, double y, double width, double height) {
this.text = text;
this.baseLineX = x;
this.baseLineY = y;
this.width = width;
this.height = height;
}
public Text(Text other) {
super(other);
this.text = other.text;
this.baseLineX = other.baseLineX;
this.baseLineY = other.baseLineY;
this.width = other.width;
this.height = other.height;
this.fontName = other.fontName;
this.fontSize = other.fontSize;
this.lineHeight = other.lineHeight;
this.align = other.align;
fillColor = other.fillColor == null ? null : other.fillColor.clone();
}
//// Getters/setters /////
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public double getBaseLineX() {
return baseLineX;
}
public void setBaseLineX(double baseLineX) {
this.baseLineX = baseLineX;
}
public double getBaseLineY() {
return baseLineY;
}
public void setBaseLineY(double baseLineY) {
this.baseLineY = baseLineY;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public String getFontName() {
return fontName;
}
public void setFontName(String fontName) {
this.fontName = fontName;
}
public double getFontSize() {
return fontSize;
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
}
public Font getFont() {
Hashtable<TextAttribute, Object> m = new Hashtable<TextAttribute, Object>();
m.put(TextAttribute.FAMILY, fontName);
m.put(TextAttribute.SIZE, fontSize);
m.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
return Font.getFont(m);
}
public double getLineHeight() {
return lineHeight;
}
public void setLineHeight(double lineHeight) {
this.lineHeight = lineHeight;
}
public Align getAlign() {
return align;
}
public void setAlign(Align align) {
this.align = align;
}
public Color getFillColor() {
return fillColor;
}
public void setFillColor(Color fillColor) {
this.fillColor = fillColor;
}
//// Font management ////
public static boolean fontExists(String fontName) {
// TODO: Move getAllFonts() in static attribute.
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] allFonts = env.getAllFonts();
for (Font font : allFonts) {
if (font.getName().equals(fontName)) {
return true;
}
}
return false;
}
//// Metrics ////
private AttributedString getStyledText(String text) {
// TODO: Find a better way to handle empty Strings (like for example paragraph line breaks)
if (text.length() == 0)
text = " ";
AttributedString attrString = new AttributedString(text);
attrString.addAttribute(TextAttribute.FONT, getFont());
if (fillColor != null)
attrString.addAttribute(TextAttribute.FOREGROUND, fillColor.getAwtColor());
if (align == Align.RIGHT) {
//attrString.addAttribute(TextAttribute.RUN_DIRECTION, TextAttribute.RUN_DIRECTION_RTL);
} else if (align == Align.CENTER) {
// TODO: Center alignment?
} else if (align == Align.JUSTIFY) {
attrString.addAttribute(TextAttribute.JUSTIFICATION, TextAttribute.JUSTIFICATION_FULL);
}
return attrString;
}
public Rect getMetrics() {
if (text == null || text.length() == 0) return new Rect();
TextLayoutIterator iterator = new TextLayoutIterator();
Rectangle2D bounds = new Rectangle2D.Double();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
// TODO: Compensate X, Y
bounds = bounds.createUnion(layout.getBounds());
}
return new Rect(bounds);
}
//// Transformations ////
protected void setupTransform(Graphics2D g) {
saveTransform(g);
AffineTransform trans = g.getTransform();
trans.concatenate(getTransform().getAffineTransform());
g.setTransform(trans);
}
public void draw(Graphics2D g) {
if (fillColor == null) return;
setupTransform(g);
if (text == null || text.length() == 0) return;
TextLayoutIterator iterator = new TextLayoutIterator();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
layout.draw(g, (float) (baseLineX + iterator.getX()), (float) (baseLineY + iterator.getY()));
}
restoreTransform(g);
}
public Path getPath() {
Path p = new Path();
p.setFillColor(fillColor == null ? null : fillColor.clone());
TextLayoutIterator iterator = new TextLayoutIterator();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
AffineTransform trans = new AffineTransform();
trans.translate(baseLineX + iterator.getX(), baseLineY + iterator.getY());
Shape shape = layout.getOutline(trans);
p.extend(shape);
}
p.transform(getTransform());
return p;
}
public boolean isEmpty() {
return text.trim().length() == 0;
}
public Rect getBounds() {
// TODO: This is correct, but creating a full path just for measuring bounds is slow.
return getPath().getBounds();
}
public Text clone() {
return new Text(this);
}
private class TextLayoutIterator implements Iterator<TextLayout> {
private double x, y;
private double ascent;
private int currentIndex = 0;
private String[] textParts;
private LineBreakMeasurer[] measurers;
private LineBreakMeasurer currentMeasurer;
private String currentText;
private boolean first;
private TextLayoutIterator() {
x = 0;
y = 0;
textParts = text.split("\n");
measurers = new LineBreakMeasurer[textParts.length];
FontRenderContext frc = new FontRenderContext(new AffineTransform(), true, true);
for (int i = 0; i < textParts.length; i++) {
AttributedString s = getStyledText(textParts[i]);
measurers[i] = new LineBreakMeasurer(s.getIterator(), frc);
}
currentMeasurer = measurers[currentIndex];
currentText = textParts[currentIndex];
first = true;
}
public boolean hasNext() {
if (currentMeasurer.getPosition() < currentText.length())
return true;
else {
currentIndex++;
if (currentIndex < textParts.length) {
currentMeasurer = measurers[currentIndex];
currentText = textParts[currentIndex];
return hasNext();
} else {
return false;
}
}
}
public TextLayout next() {
if (first) {
first = false;
} else {
y += ascent * lineHeight;
}
double layoutWidth = width == 0 ? Float.MAX_VALUE : width;
TextLayout layout = currentMeasurer.nextLayout((float) layoutWidth);
if (width == 0) {
layoutWidth = layout.getAdvance();
if (align == Align.RIGHT) {
x = -layoutWidth;
} else if (align == Align.CENTER) {
x = -layoutWidth / 2.0;
}
} else if (align == Align.RIGHT) {
x = width - layout.getAdvance();
} else if (align == Align.CENTER) {
x = (width - layout.getAdvance()) / 2.0;
} else if (align == Align.JUSTIFY) {
// Don't justify the last line.
if (currentMeasurer.getPosition() < currentText.length()) {
layout = layout.getJustifiedLayout((float) width);
}
}
ascent = layout.getAscent();
// y += layout.getDescent() + layout.getLeading() + layout.getAscent();
return layout;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void remove() {
throw new AssertionError("This operation is not implemented");
}
}
}