Open vlsi opened 4 years ago
Is this only for tests? If so I suggest doing the following, which is what I do in the RSTA unit tests, even though it leaves a bad taste in your mouth:
The issue is that the component isn't displayable. You might be able to come up with an alternative fix that dupes the control into thinking it's displayable, though IIRC I couldn't find one in a headless environment.
Is this only for tests?
It is not only for tests. The case discovered in https://github.com/apache/jmeter/pull/574 where I want to implement automatic screenshot generation for Apache JMeter. The screens that use RSTA fail in headless mode (I guess it is the only component that fails in headless).
Apparently I don't really want to augment production code with createTestGraphics()
OK. Can you please clarify why RSyntaxTextArea.paintComponent(RSyntaxTextArea.java:2141)
does not use the Graphics
object that is passed to the method?
Why does it query getGraphics()
?
What if https://github.com/bobbylight/RSyntaxTextArea/blob/cf1da1f03e380aaa7d85ab25bc1cbb3bec11a014/RSyntaxTextArea/src/main/java/org/fife/ui/rsyntaxtextarea/RSyntaxTextArea.java#L2141 is replaced with refreshFontMetrics(getGraphics2D(g))
?
It's not getGraphics2D()
that queries getGraphics()
, but rather paintComponent()
:
/**
* The <code>paintComponent</code> method is overridden so we
* apply any necessary rendering hints to the Graphics object.
*/
@Override
protected void paintComponent(Graphics g) {
// A call to refreshFontMetrics() used to be in addNotify(), but
// unfortunately we cannot always get the graphics context there. If
// the parent frame/dialog is LAF-decorated, there is a chance that the
// window's width and/or height is still == 0 at addNotify() (e.g.
// WebLaF). So unfortunately it's safest to do this here, with a flag
// to only allow it to happen once.
if (metricsNeverRefreshed) {
refreshFontMetrics(getGraphics2D(getGraphics()));
metricsNeverRefreshed = false;
}
super.paintComponent(getGraphics2D(g));
}
Per the comment, it appears there were instances where our metrics cache wasn't populated yet. We're not rendering the component to its own getGraphics()
graphics object, we're fetching it to populate our font metrics cache, in which case we need the text area's own graphics context.
I'm not clear on whether we can simply rely on any Graphics2D
instance or if we should use the one from the component when generating our font metrics cache. Using any arbitrary one just seems wrong - perhaps it has different rendering hints for example. Not sure if there is a real-world impact of doing so or not or if the difference is academic.
Using any arbitrary one just seems wrong - perhaps it has different rendering hints for example
I guess the component should paint itself with the exact rendering hints that are specified in paintComponent(Graphics g)
For example: when painting to a screen device, it could use cleartype (e.g. LCD_RGB) antialiasing, however, when printing to a png image, it could make sense to use just gray antialiasing (for better compatibility with different screens where PNG will be viewed).
Per the comment, it appears
I have seen the comment, however, it does not clarify why it uses getGraphics()
rather than input Graphics g
which is surprising :-/
That is why I'm asking.
I hear you, my only concern is because font metrics are cached, if paintComponent()
is called with some Graphics
context other than RSTA's screen-based context initially, we'll cache metrics info based on the PNG's (for example) rendering hints and use those forevermore (or until some method is called that causes us to recompute our cache).
Again, I'm not sure this is an actual problem for a couple of reasons:
paintComponent()
method called with that of a BufferedImage to save as a screenshot image) but not of the former.Judging from the JMeter defect you reference, your real-world code will not experience the NPE, is that correct? It will simply use the FontMetrics
that the realized RSTA instance used for rendering on-screen, rather than those based off of the BufferedImage (for example)'s Graphics2D
. However, unit tests will fail without some sort of workaround like I suggested earlier. Can you confirm whether these assumptions are correct?
I do face NPE when rendering the documentation.
It will simply use the FontMetrics that the realized RSTA instance used for rendering on-screen
The documentation screenshots will be rendered off-screen.
I do not see why RSTA requires on-screen so much. There's Graphics
provided.
What else does it need?
Can you provide an SSCCE that demonstrates an NPE when rendering a screenshot of an RSyntaxTextArea instance that's already rendered on-screen? It should always work - font metrics will always be cached in that case. For example:
import org.fife.ui.rtextarea.RTextScrollPane;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
public class ScreenshotTest {
private static void runScreenshotTest() {
JFrame frame = new JFrame();
frame.setLayout(new BorderLayout());
RSyntaxTextArea textArea = new RSyntaxTextArea(25, 80);
textArea.setText("public static void main(String[] args);");
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
frame.add(new RTextScrollPane(textArea));
JToolBar toolBar = new JToolBar();
JButton button = new JButton("Take Screenshot");
button.addActionListener((e) -> {
BufferedImage image = new BufferedImage(frame.getWidth(), frame.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D)image.getGraphics();
frame.getContentPane().paint(g2d);
File outputFile = new File("C:/temp/test.png");
try {
ImageIO.write(image, "PNG", outputFile);
Desktop.getDesktop().open(outputFile);
} catch (Exception ex) {
ex.printStackTrace();
}
});
toolBar.add(button);
frame.add(toolBar, BorderLayout.NORTH);
frame.pack();
frame.setLocationByPlatform(true);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
runScreenshotTest();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
Here you go:
import java.awt.Component;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
public class ScreenshotTest {
private static void runScreenshotTest() {
RSyntaxTextArea textArea = new RSyntaxTextArea(25, 80);
textArea.setText("public static void main(String[] args);");
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
textArea.setSize(new Dimension(320, 240));
// JTextArea textArea = new JTextArea();
// textArea.setText("public static void main(String[] args);");
// textArea.setSize(new Dimension(320, 240));
layoutComponent(textArea);
BufferedImage image = new BufferedImage(textArea.getWidth(), textArea.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D) image.getGraphics();
try {
textArea.paint(g2d);
} finally {
g2d.dispose();
}
File outputFile = new File("test.png");
try {
ImageIO.write(image, "PNG", outputFile);
Desktop.getDesktop().open(outputFile);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void layoutComponent(Component component) {
synchronized (component.getTreeLock()) {
component.doLayout();
if (component instanceof Container) {
for (Component child : ((Container) component).getComponents()) {
layoutComponent(child);
}
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
runScreenshotTest();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
> Task :src:core:ScreenshotTest.main()
java.lang.NullPointerException
at org.fife.ui.rsyntaxtextarea.RSyntaxTextArea.getGraphics2D(RSyntaxTextArea.java:1263)
at org.fife.ui.rsyntaxtextarea.RSyntaxTextArea.paintComponent(RSyntaxTextArea.java:2141)
at java.desktop/javax.swing.JComponent.paint(JComponent.java:1074)
at ScreenshotTest.runScreenshotTest(ScreenshotTest.java:51)
at ScreenshotTest.lambda$main$0(ScreenshotTest.java:79)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:391)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
But that example never renders the text component in screen. You're not screenshotting something a user is seeing in a running app instance, which is what I thought the original jmeter issue was discussing. It looks like you are setting it up not to create screenshots with a user interactively using the app, is that right? As that is certainly possible.
If RSTA is changed to always use the passed in graphics, note that you'll be responsible for setting rendering hints such as anti aliasing hints to make font rendering look good
But that example never renders the text component in screen
That is true. I need to generate screenshots without user interaction.
Here's a sample page: https://jmeter.apache.org/usermanual/component_reference.html#If_Controller
There are screenshots (e.g. If Controller using Variable
), and I want to make the capture automatic, so it does not require user action to take the appropriate screenshots.
If RSTA is changed to always use the passed in graphics, note that you'll be responsible for setting rendering hints
Of course, it is expected that RSTA honours the graphic settings that are passed to RSTA.
Test case (macOS, Java 13):