2424import java .awt .RenderingHints ;
2525import java .awt .font .FontRenderContext ;
2626import java .awt .font .LineBreakMeasurer ;
27+ import java .awt .font .TextHitInfo ;
2728import java .awt .font .TextLayout ;
29+ import java .awt .geom .Rectangle2D ;
2830import java .awt .image .BufferedImage ;
2931import java .io .ByteArrayOutputStream ;
3032import java .io .IOException ;
@@ -47,6 +49,7 @@ final class IndigenousTextOverlayRenderer {
4749
4850 private static final Logger LOG = Logger .getLogger (IndigenousTextOverlayRenderer .class .getName ());
4951 private static final int IMAGE_SCALE = 4 ;
52+ private static final int COMBINING_COMMA_ABOVE = 0x0313 ;
5053 private static final String FONT_BC_SANS = "BCSans" ;
5154 private static final Map <String , Font > FONT_CACHE = new ConcurrentHashMap <>();
5255
@@ -160,9 +163,9 @@ private static void drawText(final Graphics2D graphics, final JRPrintText textEl
160163 float y = topPadding + verticalOffset (textElement , contentHeight , totalHeight );
161164
162165 for (final LineLayout line : lines ) {
163- y += line .layout . getAscent ();
164- line .layout . draw (graphics , leftPadding + horizontalOffset (textElement , contentWidth , line ), y );
165- y += line .layout . getDescent () + line .layout . getLeading ();
166+ y += line .ascent ();
167+ line .draw (graphics , leftPadding + horizontalOffset (textElement , contentWidth , line ), y );
168+ y += line .descent () + line .leading ();
166169 }
167170 }
168171
@@ -183,9 +186,12 @@ private static List<LineLayout> layoutText(final String text, final Font font, f
183186 final int end = paragraph .length ();
184187
185188 while (measurer .getPosition () < end ) {
186- final TextLayout layout = measurer .nextLayout (width );
189+ final int start = measurer .getPosition ();
190+ measurer .nextLayout (width );
191+ final int lineEnd = measurer .getPosition ();
187192 final boolean lastLine = measurer .getPosition () >= end ;
188- lines .add (new LineLayout (justify (layout , width , lastLine , textElement )));
193+ final String lineText = paragraph .substring (start , lineEnd );
194+ lines .add (new LineLayout (lineText , font , frc , width , lastLine , textElement ));
189195 }
190196 }
191197
@@ -198,7 +204,8 @@ private static Font fitSingleLineFont(final String text, final Font font, final
198204 return font ;
199205 }
200206
201- final TextLayout layout = new TextLayout (text , font , frc );
207+ final String baseText = stripManualMarks (text ).text ;
208+ final TextLayout layout = new TextLayout (baseText .isEmpty () ? " " : baseText , font , frc );
202209 final float layoutHeight = layout .getAscent () + layout .getDescent () + layout .getLeading ();
203210 final float widthScale = layout .getAdvance () <= 0f ? 1f : width / layout .getAdvance ();
204211 final float heightScale = layoutHeight <= 0f ? 1f : height / layoutHeight ;
@@ -222,10 +229,10 @@ private static float horizontalOffset(final JRPrintText textElement, final float
222229 final HorizontalTextAlignEnum alignment = textElement .getHorizontalTextAlign ();
223230
224231 if (alignment == HorizontalTextAlignEnum .CENTER ) {
225- return Math .max (0f , (width - line .layout . getAdvance ()) / 2f );
232+ return Math .max (0f , (width - line .advance ()) / 2f );
226233 }
227234 if (alignment == HorizontalTextAlignEnum .RIGHT ) {
228- return Math .max (0f , width - line .layout . getAdvance ());
235+ return Math .max (0f , width - line .advance ());
229236 }
230237 return 0f ;
231238 }
@@ -345,15 +352,123 @@ private static Color hiddenTextColor(final JRPrintText textElement) {
345352 return Color .WHITE ;
346353 }
347354
355+ private static StrippedText stripManualMarks (final String text ) {
356+ final StringBuilder stripped = new StringBuilder ();
357+ final List <ManualMark > marks = new ArrayList <>();
358+ int previousBaseOffset = -1 ;
359+
360+ for (int offset = 0 ; offset < text .length ();) {
361+ final int codePoint = text .codePointAt (offset );
362+ final int charCount = Character .charCount (codePoint );
363+
364+ if (codePoint == COMBINING_COMMA_ABOVE ) {
365+ marks .add (new ManualMark (previousBaseOffset , codePoint ));
366+ } else {
367+ final int strippedOffset = stripped .length ();
368+ stripped .appendCodePoint (codePoint );
369+ if (!isCombiningMark (codePoint )) {
370+ previousBaseOffset = strippedOffset ;
371+ }
372+ }
373+
374+ offset += charCount ;
375+ }
376+
377+ return new StrippedText (stripped .toString (), marks );
378+ }
379+
380+ private static boolean isCombiningMark (final int codePoint ) {
381+ final int type = Character .getType (codePoint );
382+ return type == Character .NON_SPACING_MARK
383+ || type == Character .COMBINING_SPACING_MARK
384+ || type == Character .ENCLOSING_MARK ;
385+ }
386+
387+ private static final class StrippedText {
388+ private final String text ;
389+ private final List <ManualMark > marks ;
390+
391+ private StrippedText (final String text , final List <ManualMark > marks ) {
392+ this .text = text ;
393+ this .marks = marks ;
394+ }
395+ }
396+
397+ private static final class ManualMark {
398+ private final int previousBaseOffset ;
399+ private final int codePoint ;
400+
401+ private ManualMark (final int previousBaseOffset , final int codePoint ) {
402+ this .previousBaseOffset = previousBaseOffset ;
403+ this .codePoint = codePoint ;
404+ }
405+ }
406+
348407 private static final class LineLayout {
349- private final TextLayout layout ;
408+ private final TextLayout baseLayout ;
409+ private final List <ManualMark > marks ;
410+ private final Font font ;
411+ private final FontRenderContext fontRenderContext ;
350412
351413 private LineLayout (final TextLayout layout ) {
352- this .layout = layout ;
414+ this .baseLayout = layout ;
415+ this .marks = new ArrayList <>();
416+ this .font = null ;
417+ this .fontRenderContext = null ;
418+ }
419+
420+ private LineLayout (final String text , final Font font , final FontRenderContext fontRenderContext ,
421+ final float width , final boolean lastLine , final JRPrintText textElement ) {
422+ final StrippedText stripped = stripManualMarks (text );
423+ final String baseText = stripped .text .isEmpty () ? " " : stripped .text ;
424+ this .baseLayout = justify (new TextLayout (baseText , font , fontRenderContext ),
425+ width , lastLine , textElement );
426+ this .marks = stripped .marks ;
427+ this .font = font ;
428+ this .fontRenderContext = fontRenderContext ;
429+ }
430+
431+ private void draw (final Graphics2D graphics , final float x , final float baseline ) {
432+ baseLayout .draw (graphics , x , baseline );
433+ if (marks .isEmpty () || font == null || fontRenderContext == null ) {
434+ return ;
435+ }
436+
437+ for (final ManualMark mark : marks ) {
438+ if (mark .previousBaseOffset < 0 || mark .previousBaseOffset >= baseLayout .getCharacterCount ()) {
439+ continue ;
440+ }
441+
442+ final TextLayout markLayout = new TextLayout (new String (Character .toChars (mark .codePoint )),
443+ font , fontRenderContext );
444+ final Rectangle2D markBounds = markLayout .getBounds ();
445+ final float [] before = baseLayout .getCaretInfo (TextHitInfo .beforeOffset (mark .previousBaseOffset ));
446+ final float [] after = baseLayout .getCaretInfo (TextHitInfo .afterOffset (mark .previousBaseOffset ));
447+ final float baseGlyphCenter = (before [0 ] + after [0 ]) / 2f ;
448+ final float markX = x + baseGlyphCenter - (float ) markBounds .getCenterX ();
449+
450+ markLayout .draw (graphics , markX , baseline );
451+ }
452+ }
453+
454+ private float advance () {
455+ return baseLayout .getAdvance ();
456+ }
457+
458+ private float ascent () {
459+ return baseLayout .getAscent ();
460+ }
461+
462+ private float descent () {
463+ return baseLayout .getDescent ();
464+ }
465+
466+ private float leading () {
467+ return baseLayout .getLeading ();
353468 }
354469
355470 private float height () {
356- return layout . getAscent () + layout . getDescent () + layout . getLeading ();
471+ return ascent () + descent () + leading ();
357472 }
358473 }
359474}
0 commit comments