2

How can I auto-position text inside an image when generating one? For example, I have this method to generate my .png image:

public static void generatePNG(String message){
        try {
            int width = 400, height = 400;
            // TYPE_INT_ARGB specifies the image format: 8-bit RGBA packed
            // into integer pixels
            BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            Graphics2D ig2 = bi.createGraphics();
            Font font = new Font("TimesRoman", Font.BOLD, 15);
            ig2.setFont(font);
            FontMetrics fontMetrics = ig2.getFontMetrics();
            int stringWidth = fontMetrics.stringWidth(message);
            int stringHeight = fontMetrics.getAscent();
            ig2.setPaint(Color.black);
            ig2.drawString(message, (width - stringWidth) / 2, height / 2 + stringHeight / 4);
            ImageIO.write(bi, "PNG", new File("myimg.png"));
        } catch (IOException ie) {
            ie.printStackTrace();
        }
    }

But this centers text in my img, which was ok for testing, but now I want to add multiple lines to image and to start from top to bottom. The message I build with StringBuilder that I parse in my method is separated in new lines by System.lineSeparator() I also don't know how is it with width and height as my width must remain the same, while height can change as much as it wants, but how can I know how much will it need just by the message?

Andrew Thompson
  • 163,965
  • 36
  • 203
  • 405
sesmajster
  • 672
  • 5
  • 24

1 Answers1

1

I assume that this is an example of an XY-problem. If your goal is to generate an image, with a certain text, automatically adjusted for some font and line breaks, then you could do this on your own. You could use FontMetrics and its methods to compute the proper size of the image, and the proper locations that are then drawn with drawString calls.

But this is complicated. And it's far, far, far more complicated than it looks at the first glance. I'm not even talking about النص العربي (arabic text), but even about the seemingly most trivial elements of fonts.

The most simple solution therefore is probably to rely on the hundreds of thousands of lines of time-tested code that have already been written by experts in this field, in order to solve this problem.

Which means:

Just drop the text into a JTextArea, and then create an image from that.

The following is an MCVE that shows how this could be achieved.

GenerateTextImage

The core is the createTextImage method, which allows you to create an image from the text, with a certain font and colors. Optionally, you may specify the width of the image, and leave the daunting task of doing the line breaks to the JTextArea.

You may notice the "HTML" checkbox at the top. When it is enabled, the input is passed to a createHtmlImage method, so that you can even enter something like

<html>
This is <u>underlined</u> <br>
or in <i>italics</i>
</html>

to obtain an image of the rendered HTML output.

The full code is here:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.image.BufferedImage;

import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

public class GenerateTextImage {

    public static void main(String[] args) {

        String text = "This is a text" + "\n" 
                + "with one line that is muuuuuuuuuuuuuuuuch longer than the others" + "\n" 
                + "and some empty lines" + "\n" 
                + "\n" 
                + "\n" 
                + "as a test.";

        SwingUtilities.invokeLater(() -> createAndShowGui(text));
    }

    private static void createAndShowGui(String initialText) {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        f.getContentPane().setLayout(new BorderLayout());

        JPanel controlPanel = new JPanel();
        JCheckBox htmlCheckBox = new JCheckBox("HTML", false);
        controlPanel.add(htmlCheckBox);
        f.getContentPane().add(controlPanel, BorderLayout.NORTH);


        JPanel mainPanel = new JPanel(new GridLayout(1, 2));
        f.getContentPane().add(mainPanel, BorderLayout.CENTER);

        JTextArea inputTextArea = new JTextArea();
        JScrollPane sp0 = new JScrollPane(inputTextArea);
        sp0.setBorder(BorderFactory.createTitledBorder("Input:"));
        mainPanel.add(sp0);

        ImagePanel imagePanel = new ImagePanel();
        JScrollPane sp1 = new JScrollPane(imagePanel);
        sp1.setBorder(BorderFactory.createTitledBorder("Image:"));
        mainPanel.add(sp1);

        Runnable updateImage = () -> {
            if (!htmlCheckBox.isSelected()) {
                String text = inputTextArea.getText();
                BufferedImage image = createTextImage(text);
                imagePanel.setImage(image);
            } else {
                String text = inputTextArea.getText();
                BufferedImage image = createHtmlImage(text);
                imagePanel.setImage(image);
            }
        };

        inputTextArea.getDocument().addDocumentListener(new DocumentListener() {
            public void changedUpdate(DocumentEvent e) {
                updateImage();
            }

            public void insertUpdate(DocumentEvent e) {
                updateImage();
            }

            public void removeUpdate(DocumentEvent e) {
                updateImage();
            }

            private void updateImage() {
                updateImage.run();
            }
        });

        htmlCheckBox.addChangeListener(e -> {
            updateImage.run();
        });

        inputTextArea.setText(initialText);

        f.setSize(1200, 600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static BufferedImage createTextImage(String text) {
        return createTextImage(text, -1, new Font("TimesRoman", Font.BOLD, 15), Color.BLACK, Color.WHITE);
    }

    /**
     * Creates an image with the given text, using the given font and foreground- and background color.<br>
     * <br>
     * If the given width is not positive, then the width of the image will be computed
     * to show the longest line that appears in the text. If the given width is positive,
     * then the lines of the given text will be wrapped (at word boundaries) if possible,
     * so that the whole text can be displayed.
     * 
     * @param text The text
     * @param width The image width
     * @param font The font
     * @param foreground The foreground color
     * @param background The background color
     * @return The image
     */
    private static BufferedImage createTextImage(String text, int width, Font font, Color foreground, Color background) {
        JTextArea textArea = new JTextArea(text);
        textArea.setFont(font);
        textArea.setForeground(foreground);
        textArea.setBackground(background);
        if (width > 0) 
        {
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);
            textArea.setSize(new Dimension(width, Short.MAX_VALUE));
        }
        Dimension size = textArea.getPreferredSize();
        int w = Math.max(1, size.width);
        if (width > 0)
        {
            w = width;
        }
        int h = Math.max(1, size.height);
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        SwingUtilities.paintComponent(g, textArea, new JPanel(), 0, 0, w, h);
        g.dispose();
        return image;
    }

    private static BufferedImage createHtmlImage(String text) {
        return createHtmlImage(text, new Font("TimesRoman", Font.BOLD, 15), Color.BLACK, Color.WHITE);
    }

    /**
     * Creates an image with the given HTML string, using the given font and foreground- and background color.<br>
     * 
     * @param html The HTML string
     * @param font The font
     * @param foreground The foreground color
     * @param background The background color
     * @return The image
     */
    private static BufferedImage createHtmlImage(String html, Font font, Color foreground, Color background) {
        JLabel label = new JLabel(html);
        label.setOpaque(true);
        label.setFont(font);
        label.setForeground(foreground);
        label.setBackground(background);
        Dimension size = label.getPreferredSize();
        int w = Math.max(1, size.width);
        int h = Math.max(1, size.height);
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        SwingUtilities.paintComponent(g, label, new JPanel(), 0, 0, w, h);
        g.dispose();
        return image;
    }


    static class ImagePanel extends JPanel {
        private static final long serialVersionUID = 1L;
        private BufferedImage image;

        public void setImage(BufferedImage image) {
            this.image = image;
            repaint();
        }

        @Override
        public Dimension getPreferredSize() {
            if (image == null || super.isPreferredSizeSet()) {
                return super.getPreferredSize();
            }
            return new Dimension(image.getWidth(), image.getHeight());
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (image != null) {
                g.drawImage(image, 0, 0, null);
            }
        }
    }
}

Edit: A small addendum for one of the comments. It centers the text horizontally, using the snippet from https://stackoverflow.com/a/3213361/3182664 . Note that this is not tested thoroughly. At some point, questions, comments and edits boil down to "Write some code for me". I'm a freelancer. You can hire me.

private static BufferedImage createTextImage(String text, int width, Font font, Color foreground, Color background) {
    JTextPane textPane = new JTextPane();
    textPane.setText(text);

    // See https://stackoverflow.com/a/3213361/3182664
    StyledDocument doc = textPane.getStyledDocument();
    SimpleAttributeSet center = new SimpleAttributeSet();
    StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
    doc.setParagraphAttributes(0, doc.getLength(), center, false);

    textPane.setFont(font);
    textPane.setForeground(foreground);
    textPane.setBackground(background);
    if (width > 0) 
    {
        //textPane.setLineWrap(true);
        //textPane.setWrapStyleWord(true);
        textPane.setSize(new Dimension(width, Short.MAX_VALUE));
    }
    Dimension size = textPane.getPreferredSize();
    int w = Math.max(1, size.width);
    if (width > 0)
    {
        w = width;
    }
    int h = Math.max(1, size.height);
    BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g = image.createGraphics();
    SwingUtilities.paintComponent(g, textPane, new JPanel(), 0, 0, w, h);
    g.dispose();
    return image;
}
Marco13
  • 50,927
  • 9
  • 71
  • 148
  • I managed to make this solution work, but for some reason I can't change the font size. I doesn't matter what number I put for 'size' in font in createTextImage but it doesn't change anything. I'm using this to print directly to printer and the print output is always the same. I can see in the gui thou, that it changes. – sesmajster Jul 31 '19 at 09:30
  • @user3029612 I'm not sure whether I understood this correctly. You are changing the font size (e.g. from `15` in the example to, say `25` or so), and you see that the larger font is used for creating the image, right? Then, where does the printer come into play? Are you then printing the resulting `PNG` file, or are you somehow using the Java print API? – Marco13 Jul 31 '19 at 12:50
  • I'm printing an InputStream which I convert from BufferedImage that your method (createTextImage) creates. For printing I'm using 'PrintService' from javax.print.* In the GUI that you create I can see that the text is bigger, but when printing it's always the same size. – sesmajster Aug 01 '19 at 10:10
  • @user3029612 It's hard to tell what's wrong there. The method that I showed created an image. You can define the font size. When the font is larger, then the image is larger. **Iff** the image is always printed at the same size, then I guess that it is related to the way how you are printing it. You might consider asking this as a dedicated question, but it's not unlikely that it would be dovnvoted or closed as "seeking for debugging help"... – Marco13 Aug 01 '19 at 11:02
  • I'm debugging this myself. I have one more additional question thou. How do you center text? Could I parse in the createTextImage array of objects that have 2 attributes, string and position, so the method will append string of obect according to the position that would be "Left", "Center" or "Right"? – sesmajster Aug 01 '19 at 12:10
  • @user3029612 You cannot arbitrarily change or extend the question. If you write a *clear* question, one can write a *clear* answer. However, I have added another implementation of the `createTextImage` method that centers the text. Many details would have to be reviewed there, but I guess that doesn't matter as long as it shows the behavior that you need right now. – Marco13 Aug 01 '19 at 12:23