Justify left/center/right aligned text


public class TextMetrics
{
   public static final int ALIGN_LEFT   = -1;
   public static final int ALIGN_CENTER = -0;
   public static final int ALIGN_RIGHT  = +1;

   public TextMetrics(String text)
   {
      this.text = text;
   }

   //

   private final String text;

   public String getText()
   {
      return this.text;
   }

   //

   private int width;

   public void setWidth(int width)
   {
      this.width = width;
   }

   public int getWidth()
   {
      return this.width;
   }

   //

   private int alignment;

   public void setAlignment(int alignment)
   {
      this.alignment = alignment;
   }

   public int getAlignment()
   {
      return this.alignment;
   }

   //

   private boolean justify;

   public void setJustified(boolean justify)
   {
      this.justify = justify;
   }

   public boolean isJustified()
   {
      return this.justify;
   }

   //

   private Font font;

   public void setFont(Font font)
   {
      this.font = font;
   }

   public Font getFont()
   {
      return this.font;
   }

   //

   public List<WordMetric> calculate()
   {
      List<LineMetric> lines = calculateLineMetrics(this.text, this.font, this.width);
      this.lineCount = lines.size();
      return calculateWords(lines, this.font, this.width, this.alignment, this.justify);
   }

   private int lineCount;

   public int getLineCount()
   {
      return this.lineCount;
   }

   public int getLineHeight()
   {
      return getMetrics(this.font).getHeight();
   }

   //

   static ThreadLocal<Graphics>                               gtl;
   static ThreadLocal<Map<Font, Map<Character, Rectangle2D>>> mtl;

   static
   {
      gtl = new ThreadLocal<Graphics>()
      {
         @Override
         protected Graphics initialValue()
         {
            return new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB).getGraphics();
         }
      };

      mtl = new ThreadLocal<Map<Font, Map<Character, Rectangle2D>>>()
      {
         @Override
         protected Map<Font, Map<Character, Rectangle2D>> initialValue()
         {
            return new HashMap<Font, Map<Character, Rectangle2D>>();
         }
      };
   }

   public static FontMetrics getMetrics(Font font)
   {
      return gtl.get().getFontMetrics(font);
   }

   public static Rectangle2D getArea(Font font, char c)
   {
      Map<Character, Rectangle2D> charToArea = mtl.get().get(font);
      if (charToArea == null)
      {
         mtl.get().put(font, charToArea = new HashMap<Character, Rectangle2D>());
      }

      Rectangle2D area = charToArea.get(Character.valueOf(c));
      if (area == null)
      {
         Graphics g = gtl.get();
         FontMetrics fm = g.getFontMetrics(font);
         area = fm.getStringBounds(new char[] { c }, 0, 1, g);
         charToArea.put(Character.valueOf(c), area);
      }

      return area;
   }

   public static List<WordMetric> calculateWords(List<LineMetric> lines, Font font, int maxWidth, int align, boolean justify)
   {
      int h = getMetrics(font).getHeight();
      int y = getMetrics(font).getAscent();

      List<WordMetric> segments = new ArrayList<WordMetric>();

      for (LineMetric trio : lines)
      {
         String lineText = trio.text;
         double lineWidth = trio.width;
         boolean lineFeed = trio.lineFeed;

         if (justify && !lineFeed)
         {
            CharFeeder stuff2 = new CharFeeder(font);
            for (char c : lineText.toCharArray())
               stuff2.feed(c);
            if (stuff2.currentWord.length() != 0)
               stuff2.end(true);

            double toFill = maxWidth - lineWidth;

            int spaceCount = 0;
            for (LineMetric word : stuff2.words)
               if (word.text.equals(" "))
                  spaceCount++;
            for (LineMetric word : stuff2.words)
               if (word.text.equals(" "))
                  word.width += (toFill / spaceCount);

            lineWidth = 0.0;
            for (LineMetric word : stuff2.words)
               lineWidth += word.width;

            double x;
            if (align == -1)
               x = (maxWidth - lineWidth) * 0.0;
            else if (align == ALIGN_CENTER)
               x = (maxWidth - lineWidth) * 0.5;
            else if (align == ALIGN_RIGHT)
               x = (maxWidth - lineWidth) * 1.0;
            else
               throw new IllegalStateException();

            for (LineMetric word : stuff2.words)
            {
               if (!word.text.trim().isEmpty())
                  segments.add(new WordMetric(word.text, x, y));
               x += word.width;
            }
         }
         else
         {
            double x;
            if (align == ALIGN_LEFT)
               x = (maxWidth - lineWidth) * 0.0;
            else if (align == ALIGN_CENTER)
               x = (maxWidth - lineWidth) * 0.5;
            else if (align == ALIGN_RIGHT)
               x = (maxWidth - lineWidth) * 1.0;
            else
               throw new IllegalStateException();

            segments.add(new WordMetric(lineText, x, y));
         }
         y += h;
      }

      return segments;
   }

   public static List<LineMetric> calculateLineMetrics(String input, Font font, double maxWidth)
   {
      CharFeeder stuff = new CharFeeder(font);

      for (char c : input.toCharArray())
      {
         stuff.feed(c);
      }
      if (stuff.currentWord.length() != 0)
      {
         stuff.end(true);
      }

      List<LineMetric> lines;
      lines = new ArrayList<LineMetric>();

      double currentWidth = 0.0;
      StringBuilder currentLine = new StringBuilder();
      for (LineMetric word : stuff.words)
      {
         String wordText = word.text;
         double wordWidth = word.width;
         boolean lineFeed = word.lineFeed;

         if (currentLine.length() == 0 && wordText.equals(" "))
            wordWidth = 0.0;

         if (currentWidth + wordWidth > maxWidth)
         {
            String line = currentLine.toString();
            if (!line.isEmpty() && line.charAt(0) == ' ')
            {
               line = line.substring(1);
               currentWidth -= getArea(font, ' ').getWidth();
            }
            if (!line.isEmpty() && line.charAt(line.length() - 1) == ' ')
            {
               line = line.substring(0, line.length() - 1);
               currentWidth -= getArea(font, ' ').getWidth();
            }
            lines.add(new LineMetric(line, currentWidth, false));
            currentLine.setLength(0);
            currentLine.trimToSize();
            currentWidth = 0.0;
         }

         currentWidth += wordWidth;
         currentLine.append(wordText);

         if (lineFeed)
         {
            String line = currentLine.toString();
            if (!line.isEmpty() && line.charAt(0) == ' ')
            {
               line = line.substring(1);
               currentWidth -= getArea(font, ' ').getWidth();
            }
            if (!line.isEmpty() && line.charAt(line.length() - 1) == ' ')
            {
               line = line.substring(0, line.length() - 1);
               currentWidth -= getArea(font, ' ').getWidth();
            }
            lines.add(new LineMetric(line, currentWidth, true));
            currentLine.setLength(0);
            currentLine.trimToSize();
            currentWidth = 0.0;
         }
      }

      if (currentLine.length() != 0)
      {
         String line = currentLine.toString();
         if (!line.isEmpty() && line.charAt(0) == ' ')
         {
            line = line.substring(1);
            currentWidth -= getArea(font, ' ').getWidth();
         }
         if (!line.isEmpty() && line.charAt(line.length() - 1) == ' ')
         {
            line = line.substring(0, line.length() - 1);
            currentWidth -= getArea(font, ' ').getWidth();
         }
         lines.add(new LineMetric(line, currentWidth, true));
         currentLine.setLength(0);
         currentLine.trimToSize();
         currentWidth = 0.0;
      }

      return lines;
   }

   static class CharFeeder
   {
      public final Font             font;
      public final List<LineMetric> words;

      public CharFeeder(Font font)
      {
         this.font = font;
         this.words = new ArrayList<LineMetric>();
      }

      double        currentWidth = 0.0;
      StringBuilder currentWord  = new StringBuilder();

      public void feed(char c)
      {
         if (c == '\n')
         {
            this.end(true);
            return;
         }

         if (c < ' ')
         {
            throw new IllegalStateException("c=" + (int) c);
         }

         Rectangle2D area = TextMetrics.getArea(this.font, c);

         if (c == ' ')
         {
            this.end(false);
         }

         this.currentWord.append(c);
         this.currentWidth += area.getWidth();

         if (c == ' ')
         {
            this.end(false);
         }
      }

      public void end(boolean lineFeed)
      {
         String word = this.currentWord.toString();

         this.words.add(new LineMetric(word, this.currentWidth, lineFeed));

         this.currentWord.setLength(0);
         this.currentWord.trimToSize();
         this.currentWidth = 0.0;
      }
   }
}

How to use


      String text = "...";

      TextMetrics metrics = new TextMetrics(text);
      metrics.setFont(new Font("Times New Roman", Font.ITALIC, 16));
      metrics.setAlignment(ALIGN_RIGHT);
      metrics.setJustified(true);
      metrics.setWidth(500);

      List<WordMetric> words = metrics.calculate();
      int lineCount = metrics.getLineCount();
      int lineHeight = metrics.getLineHeight();


      int totalHeight = lineHeight * lineCount;
      int m = 32;
      int w = m + metrics.getWidth() + m;
      int h = m + totalHeight + m;
      BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
      Graphics g = img.getGraphics();

      g.setColor(Color.WHITE);
      g.fillRect(0, 0, img.getWidth(), img.getHeight());
      g.setColor(Color.BLACK);
      GraphicsUtil.enableAA(g); // mess with RenderingHints


      g.translate(+m, +m);
      g.setFont(metrics.getFont());
      for (WordMetric word : words)
         g.drawString(word.text, (int) Math.round(word.x), (int) Math.round(word.y));
      g.translate(-m, -m);

      ImageIO.write(img, "PNG", new File("./output.png"));

could you please post WordMetric and LineMetric?

Bwa! :o They used to be inner classes, so I assumed they were in the first post. :persecutioncomplex:


public class LineMetric
{
   public String  text;
   public double  width;
   public boolean lineFeed;

   public LineMetric(String text, double width, boolean lineFeed)
   {
      this.text = text;
      this.width = width;
      this.lineFeed = lineFeed;
   }
}

public class WordMetric
{
   public String text;
   public double x, y;

   public WordMetric(String text, double x, double y)
   {
      this.text = text;
      this.x = x;
      this.y = y;
   }
}

Thanks, nice work :slight_smile: