1 /*
2  * JasperReports - Free Java Reporting Library.
3  * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
4  * http://www.jaspersoft.com
5  *
6  * Unless you have purchased a commercial license agreement from Jaspersoft,
7  * the following license terms apply:
8  *
9  * This program is part of JasperReports.
10  *
11  * JasperReports is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * JasperReports is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
23  */

24 package net.sf.jasperreports.engine.util;
25
26 import java.awt.Font;
27 import java.awt.font.TextAttribute;
28 import java.awt.geom.AffineTransform;
29 import java.text.AttributedCharacterIterator;
30 import java.text.AttributedCharacterIterator.Attribute;
31 import java.text.AttributedString;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 import java.util.Set;
41
42 import net.sf.jasperreports.annotations.properties.Property;
43 import net.sf.jasperreports.annotations.properties.PropertyScope;
44 import net.sf.jasperreports.engine.DefaultJasperReportsContext;
45 import net.sf.jasperreports.engine.JRPropertiesUtil;
46 import net.sf.jasperreports.engine.JRRuntimeException;
47 import net.sf.jasperreports.engine.JasperReportsContext;
48 import net.sf.jasperreports.engine.fonts.AwtFontAttribute;
49 import net.sf.jasperreports.engine.fonts.FontUtil;
50 import net.sf.jasperreports.properties.PropertyConstants;
51
52
53 /**
54  * @author Teodor Danciu (teodord@users.sourceforge.net)
55  */

56 public class JRStyledText implements Cloneable
57 {
58     public static final String EXCEPTION_MESSAGE_KEY_CANNOT_COPY_CHARACTERS = "util.styled.text.cannot.copy.characters";
59     /**
60      * 
61      */

62     @Property(
63             valueType = Boolean.class,
64             defaultValue = PropertyConstants.BOOLEAN_FALSE,
65             scopes = {PropertyScope.CONTEXT, PropertyScope.REPORT},
66             sinceVersion = PropertyConstants.VERSION_3_6_1
67             )
68     public static final String PROPERTY_AWT_IGNORE_MISSING_FONT = JRPropertiesUtil.PROPERTY_PREFIX + "awt.ignore.missing.font";
69     
70     @Property(
71             valueType = Boolean.class,
72             scopes = {PropertyScope.GLOBAL},
73             sinceVersion = PropertyConstants.VERSION_3_1_3
74             )
75     public static final String PROPERTY_AWT_SUPERSCRIPT_FIX_ENABLED = JRPropertiesUtil.PROPERTY_PREFIX + "awt.superscript.fix.enabled";
76
77     private static final boolean AWT_SUPERSCRIPT_FIX_ENABLED = 
78         JRPropertiesUtil.getInstance(DefaultJasperReportsContext.getInstance()).getBooleanProperty(PROPERTY_AWT_SUPERSCRIPT_FIX_ENABLED);
79     
80     private static final Set<Attribute> FONT_ATTRS = new HashSet<Attribute>();
81     static
82     {
83         FONT_ATTRS.add(TextAttribute.FAMILY);
84         FONT_ATTRS.add(JRTextAttribute.FONT_INFO);
85         FONT_ATTRS.add(TextAttribute.WEIGHT);
86         FONT_ATTRS.add(TextAttribute.POSTURE);
87         FONT_ATTRS.add(TextAttribute.SIZE);
88         FONT_ATTRS.add(TextAttribute.SUPERSCRIPT);
89     }
90     
91     /**
92      *
93      */

94     private StringBuilder sbuffer;
95     private String text;
96     
97     private List<Run> runs = Collections.emptyList();
98     private AttributedString attributedString;
99     private AttributedString awtAttributedString;
100     private Map<Attribute,Object> globalAttributes;
101     private Locale locale;
102
103     
104     /**
105      *
106      */

107     public JRStyledText()
108     {
109         this(null);
110     }
111
112
113     /**
114      *
115      */

116     public JRStyledText(Locale locale)
117     {
118         this.locale = locale;
119     }
120
121     public JRStyledText(Locale locale, String text)
122     {
123         this.locale = locale;
124         this.text = text;
125     }
126
127     public JRStyledText(Locale locale, String text, Map<Attribute,Object> globalAttributes)
128     {
129         this.locale = locale;
130         this.text = text;
131         this.globalAttributes = globalAttributes;
132         this.runs = Collections.singletonList(new Run(globalAttributes, 0, text.length()));
133     }
134     
135     private void ensureBuffer()
136     {
137         if (sbuffer == null)
138         {
139             sbuffer = text == null ? new StringBuilder() : new StringBuilder(text);
140         }
141         text = null;
142     }
143     
144     private void ensureText()
145     {
146         if (text == null)
147         {
148             text = sbuffer == null ? "" : sbuffer.toString();
149         }
150         sbuffer = null;
151     }
152
153
154     /**
155      *
156      */

157     public void append(String text)
158     {
159         ensureBuffer();
160         sbuffer.append(text);
161         attributedString = null;
162         awtAttributedString = null;
163     }
164
165     /**
166      *
167      */

168     public void addRun(Run run)
169     {
170         int currentSize = runs.size();
171         if (currentSize == 0)
172         {
173             runs = Collections.singletonList(run);
174         }
175         else
176         {
177             if (currentSize == 1 && !(runs instanceof ArrayList))
178             {
179                 List<Run> newRuns = new ArrayList<Run>();
180                 newRuns.add(runs.get(0));
181                 runs = newRuns;
182             }
183
184             runs.add(run);
185         }
186
187         attributedString = null;
188         awtAttributedString = null;
189     }
190
191     /**
192      *
193      */

194     public int length()
195     {
196         return text == null ? (sbuffer == null ? 0 : sbuffer.length()) : text.length();
197     }
198
199     /**
200      *
201      */

202     public String getText()
203     {
204         ensureText();
205         return text;
206     }
207
208     /**
209      *
210      */

211     public Locale getLocale()
212     {
213         return locale;
214     }
215
216     /**
217      *
218      */

219     public AttributedString getAttributedString()
220     {
221         if (attributedString == null)
222         {
223             ensureText();
224             attributedString = new AttributedString(text);
225
226             for(int i = runs.size() - 1; i >= 0; i--)
227             {
228                 Run run = runs.get(i);
229                 if (run.startIndex != run.endIndex && run.attributes != null)
230                 {
231                     attributedString.addAttributes(run.attributes, run.startIndex, run.endIndex);
232                 }
233             }
234         }
235         
236         return attributedString;
237     }
238
239     /**
240      * Returns an attributed string that contains the AWT font attribute, as the font is actually loaded.
241      */

242     public AttributedString getAwtAttributedString(JasperReportsContext jasperReportsContext, boolean ignoreMissingFont)
243     {
244         if (awtAttributedString == null)
245         {
246             ensureText();
247             awtAttributedString = new AttributedString(text);
248
249             for(int i = runs.size() - 1; i >= 0; i--)
250             {
251                 Run run = runs.get(i);
252                 if (run.startIndex != run.endIndex && run.attributes != null)
253                 {
254                     awtAttributedString.addAttributes(run.attributes, run.startIndex, run.endIndex);
255                 }
256 //                if (
257 //                    run.startIndex != run.endIndex 
258 //                    && run.attributes != null 
259 //                    && !run.attributes.isEmpty()
260 //                    )
261 //                {
262 //                    for (Iterator it = run.attributes.entrySet().iterator(); it.hasNext();)
263 //                    {
264 //                        Map.Entry entry = (Map.Entry) it.next();
265 //                        AttributedCharacterIterator.Attribute attribute = 
266 //                            (AttributedCharacterIterator.Attribute) entry.getKey();
267 //                        if (!(attribute instanceof JRTextAttribute))
268 //                        {
269 //                            Object value = entry.getValue();
270 //                            awtAttributedString.addAttribute(attribute, value, run.startIndex, run.endIndex);
271 //                        }
272 //                    }
273 //                }
274             }
275             
276             AttributedCharacterIterator iterator = awtAttributedString.getIterator();
277             
278             int runLimit = 0;
279             AffineTransform atrans = null;
280
281             while(runLimit < iterator.getEndIndex() && (runLimit = iterator.getRunLimit(FONT_ATTRS)) <= iterator.getEndIndex())
282             {
283                 Map<Attribute,Object> attrs = iterator.getAttributes();
284                     
285                 AwtFontAttribute fontAttribute = AwtFontAttribute.fromAttributes(attrs);
286                 
287                 FontUtil fontUtil = FontUtil.getInstance(jasperReportsContext);
288                 Font awtFont = fontUtil.getAwtFontFromBundles(
289                         fontAttribute, 
290                         ((TextAttribute.WEIGHT_BOLD.equals(attrs.get(TextAttribute.WEIGHT))?Font.BOLD:Font.PLAIN)
291                             |(TextAttribute.POSTURE_OBLIQUE.equals(attrs.get(TextAttribute.POSTURE))?Font.ITALIC:Font.PLAIN)), 
292                         (Float)attrs.get(TextAttribute.SIZE),
293                         locale,
294                         ignoreMissingFont
295                         );
296                 if (awtFont == null)
297                 {
298                     // The font was not found in any of the font extensions, so it is expected that the TextAttribute.FAMILY attribute
299                     // will be used by AWT. In that case, we want make sure the font family name is available to the JVM.
300                     fontUtil.checkAwtFont(fontAttribute.getFamily(), ignoreMissingFont);
301                 }
302                 else
303                 {
304                     if (AWT_SUPERSCRIPT_FIX_ENABLED && atrans != null)
305                     {
306                         double y = atrans.getTranslateY();
307                         atrans = new AffineTransform();
308                         atrans.translate(0, - y);
309                         awtFont = awtFont.deriveFont(atrans);
310                         atrans = null;
311                     }
312                     Integer superscript = (Integer)attrs.get(TextAttribute.SUPERSCRIPT);
313                     if (TextAttribute.SUPERSCRIPT_SUPER.equals(superscript))
314                     {
315                         atrans = new AffineTransform();
316                         atrans.scale(2 / 3d, 2 / 3d);
317                         atrans.translate(0, - awtFont.getSize() / 2f);
318                         awtFont = awtFont.deriveFont(atrans);
319                     }
320                     else if (TextAttribute.SUPERSCRIPT_SUB.equals(superscript))
321                     {
322                         atrans = new AffineTransform();
323                         atrans.scale(2 / 3d, 2 / 3d);
324                         atrans.translate(0, awtFont.getSize() / 2f);
325                         awtFont = awtFont.deriveFont(atrans);
326                     }
327                     awtAttributedString.addAttribute(TextAttribute.FONT, awtFont, iterator.getIndex(), runLimit);
328                 }
329                 
330                 iterator.setIndex(runLimit);
331             }
332
333         }
334         
335         return awtAttributedString;
336     }
337
338
339     /**
340      *
341      */

342     public List<Run> getRuns()
343     {
344         return runs;
345     }
346
347     /**
348      * 
349      */

350     public static class Run implements Cloneable
351     {
352         /**
353          *
354          */

355         public Map<Attribute,Object> attributes;
356         public int startIndex;
357         public int endIndex;
358
359         /**
360          *
361          */

362         public Run(Map<Attribute,Object> attributes, int startIndex, int endIndex) 
363         {
364             this.attributes = attributes;
365             this.startIndex = startIndex;
366             this.endIndex = endIndex;
367         }
368
369         @Override
370         protected Object clone()
371         {
372             return cloneRun();
373         }
374         
375         /**
376          * Clones this object.
377          * 
378          * @return a clone of this object
379          */

380         public Run cloneRun()
381         {
382             try
383             {
384                 Run clone = (Run) super.clone();
385                 clone.attributes = cloneAttributesMap(attributes);
386                 return clone;
387             }
388             catch (CloneNotSupportedException e)
389             {
390                 // never
391                 throw new JRRuntimeException(e);
392             }
393         }
394     }
395
396     public void setGlobalAttributes(Map<Attribute,Object> attributes)
397     {
398         this.globalAttributes = attributes;
399         addRun(new Run(attributes, 0, length()));
400     }
401     
402     
403     public Map<Attribute,Object> getGlobalAttributes()
404     {
405         return globalAttributes;
406     }
407     
408     @Override
409     protected Object clone() throws CloneNotSupportedException
410     {
411         // TODO Auto-generated method stub
412         return super.clone();
413     }
414     
415     protected static Map<Attribute,Object> cloneAttributesMap(Map<Attribute,Object> attributes)
416     {
417         return attributes == null ? null : new HashMap<Attribute,Object>(attributes);
418     }
419
420     
421     /**
422      * Clones this object.
423      * 
424      * @return a clone of this object
425      */

426     public JRStyledText cloneText()
427     {
428         try
429         {
430             JRStyledText clone = (JRStyledText) super.clone();
431             clone.globalAttributes = cloneAttributesMap(globalAttributes);
432             
433             int runsCount = runs.size();
434             if (runsCount == 0)
435             {
436                 clone.runs = Collections.emptyList();
437             }
438             else if (runsCount == 1)
439             {
440                 clone.runs = Collections.singletonList(runs.get(0).cloneRun());
441             }
442             else
443             {
444                 clone.runs = new ArrayList<Run>(runsCount);
445                 for (Iterator<Run> it = runs.iterator(); it.hasNext();)
446                 {
447                     Run run = it.next();
448                     Run runClone = run.cloneRun();
449                     clone.runs.add(runClone);
450                 }
451             }
452             
453             return clone;
454         }
455         catch (CloneNotSupportedException e)
456         {
457             // never
458             throw new JRRuntimeException(e);
459         }
460     }
461     
462     /**
463      * Inserts a string at specified positions in the styled text.
464      * 
465      * <p>
466      * The string is inserted in the style runs located at the insertion
467      * positions.  If a style run finished right before the insertion position,
468      * the string will be part of this run (but not of the runs that start
469      * right after the insertion position).
470      * 
471      * @param str the string to insert
472      * @param offsets the incremental offsets of the positions at which to
473      * insert the string
474      */

475     public void insert(String str, short[] offsets)
476     {
477         int insertLength = str.length();
478         
479         int currentLength = length();
480         //new buffer to do the insertion
481         StringBuilder newText = new StringBuilder(currentLength + insertLength * offsets.length); //NOPMD
482         char[] buffer = null;
483         int offset = 0;
484         for (int i = 0; i < offsets.length; i++)
485         {
486             int charCount = offsets[i];
487             int prevOffset = offset;
488             offset += offsets[i];
489             
490             //append chunk of text
491             if (buffer == null || buffer.length < charCount)
492             {
493                 buffer = new char[charCount];
494             }
495             getChars(prevOffset, offset, buffer, 0);
496             newText.append(buffer, 0, charCount);
497             
498             //append inserted text
499             newText.append(str);
500             
501             //adjust runs
502             //TODO optimize this?
503             for (Iterator<Run> it = runs.iterator(); it.hasNext();)
504             {
505                 Run run = it.next();
506                 if (run.startIndex >= offset)
507                 {
508                     //inserted before run
509                     run.startIndex += insertLength;
510                     run.endIndex += insertLength;
511                 }
512                 else if (run.endIndex >= offset)
513                 {
514                     //inserted in the middle or immediately after a run
515                     //the inserted text is included in the run
516                     run.endIndex += insertLength;
517                 }
518             }
519         }
520         
521         //append remaining text
522         int charCount = currentLength - offset;
523         if (buffer == null || buffer.length < charCount)
524         {
525             buffer = new char[charCount];
526         }
527         getChars(offset, currentLength, buffer, 0);
528         newText.append(buffer, 0, charCount);
529         
530         //overwrite with the inserted buffer
531         sbuffer = newText;
532         text = null;
533         
534         attributedString = null;
535         awtAttributedString = null;
536     }
537     
538     private void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
539     {
540         if (text != null)
541         {
542             text.getChars(srcBegin, srcEnd, dst, dstBegin);
543         }
544         else if (sbuffer != null)
545         {
546             sbuffer.getChars(srcBegin, srcEnd, dst, dstBegin);
547         }
548         else if (srcBegin < srcEnd)
549         {
550             // should not happen
551             throw 
552                 new JRRuntimeException(
553                     EXCEPTION_MESSAGE_KEY_CANNOT_COPY_CHARACTERS,
554                     new Object[]{srcBegin, srcEnd});
555         }
556     }
557 }
558