Java Swing Metal Look & Feel title bar printing bug fix

bug description

Swingの印刷機能を用いるとき、Mac OS XVMでは、次の状況で、例外が発生する.

  • MetalのLook&Feelを使っている
  • JInternalFrame等、タイトルバーがウィンドウの内部に描画されている
  • ウィンドウ内部を printAll() メソッド等で印刷しようとする

印刷物上の,タイトルバーの描画が不完全となる.さらに残りの部分が印刷されない.

同様の振る舞いは Apple のMLでも報告されている:http://lists.apple.com/archives/java-dev/2003/Jul/msg00069.html

stack trace

java.lang.NullPointerException
	at javax.swing.plaf.metal.BumpBuffer.fillBumpBuffer(MetalBumps.java:195)
	at javax.swing.plaf.metal.BumpBuffer.<init>(MetalBumps.java:160)
	at javax.swing.plaf.metal.MetalBumps.getBuffer(MetalBumps.java:78)
	at javax.swing.plaf.metal.MetalBumps.paintIcon(MetalBumps.java:109)
	at javax.swing.plaf.metal.MetalInternalFrameTitlePane.paintComponent(MetalInternalFrameTitlePane.java:454)
	at javax.swing.JComponent.paint(JComponent.java:1006)
:
:

cause (原因)

  • タイトルバーに表現された凸凹(bump) を描画するためのバッファを作成するために、Swingは GraphicsConfiguration#createCompatibleImage(int,int.int) を呼ぶ
  • MacOSX の Swingでは、このメソッドは、apple.awt.CPrinterGraphicsConfig#createCompatibleImage(int,int.int) であるが、このメソッドは常に null を返す
  • javax.swing.plaf.metal.BumpBuffer.fillBumpBuffer は、null であるイメージオブジェクトに対してbumpの描画を試み、NullPointerExceptionが発生する

fix (回避方法)

後で示すコード例のように、印刷時に使う Graphics2D のクラスにラッパーをかぶせ、 createCompatibleImageの呼び出し時に nullではなく適切な BufferedImageを返すようにする.

validity (このfixの正当性)

互換性のあるカラーマップから BufferedImage を生成するため、タイトルバーが正しく描画されることを確認した.

確認用コード

/**
 * AppleのJVMで ・Look&Feelが Metalのとき ・JInternalFrameがウィンドウ内部にあるとき
 * ・内部フレームのタイトルバーの描画に失敗して例外が発生する という振る舞いを防止する。 printメソッドを参照のこと。
 * 
 * @author keigoi
 * 
 */
public class OSXMetalPrintingBugTest extends JFrame implements Printable {
    private static final long serialVersionUID = -1L;

    /**
     * 印刷用メソッド。ここに細工する。
     */
    public int print(Graphics g_, PageFormat pf, int page)
            throws PrinterException {
        if (page > 0) {
            return Printable.NO_SUCH_PAGE;
        }
        Graphics2D g = (Graphics2D) g_;
        g.translate(pf.getImageableX(), pf.getImageableY());
        double scale = Math.min(pf.getImageableWidth() / this.getWidth(), pf
                .getImageableHeight()
                / this.getHeight());
        g.scale(scale, scale);

        // ここで proxy をかました Graphics2Dオブジェクトを渡す
        this.printAll(new MyG((Graphics2D) g));
        // this.printAll(g); // <-- 例外発生!

        return Printable.PAGE_EXISTS;
    }

    /**
     * ウィンドウ内部に JInternalFrameをもつ JFrameを作る
     */
    public OSXMetalPrintingBugTest() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JDesktopPane pane = new JDesktopPane();
        pane.add(createIFrame());
        setContentPane(pane);
    }

    /**
     * 印刷ボタンを含んだ JInternalFrameを作る。
     * 
     * @return
     */
    JInternalFrame createIFrame() {
        JInternalFrame internal = new JInternalFrame("internal frame", true,
                true, true, true);
        internal.setLayout(new FlowLayout());
        internal.getContentPane().add(createButton());
        internal.pack();
        internal.setSize(300, 200);
        internal.setVisible(true);
        return internal;
    }

    /**
     * 印刷ボタンを作る。
     * 
     * @return ボタン
     */
    JButton createButton() {
        JButton button = new JButton("Let's print!");
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                PrinterJob job = PrinterJob.getPrinterJob();
                PageFormat pf = job.defaultPage();
                pf.setOrientation(PageFormat.LANDSCAPE);
                job.setPrintable(OSXMetalPrintingBugTest.this, pf);
                boolean ok = job.printDialog();
                if (ok) {
                    try {
                        job.print();
                    } catch (PrinterException ex) {
                        // pass
                    }
                }

            }
        });
        return button;
    }

    public static void main(String[] args) throws Throwable {
        UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
        final OSXMetalPrintingBugTest frame = new OSXMetalPrintingBugTest();
        frame.setSize(320, 240);
        frame.setVisible(true);
    }

}

/**
 * Graphics2D の getDeviceConfiguration 関数をフックして、プロキシをかましたオブジェクトを返すように 修正したクラス。
 * createで作成するオブジェクトにもプロキシをかましている。
 * 
 * @author keigoi
 */
class MyG extends Graphics2D {
    Graphics2D g;

    public MyG(Graphics2D g) {
        this.g = g;
    }

    @Override
    public Graphics create(int x, int y, int width, int height) {
        return new MyG((Graphics2D) g.create(x, y, width, height));
    }

    @Override
    public Graphics create() {
        return new MyG((Graphics2D) g.create());
    }

    @Override
    public GraphicsConfiguration getDeviceConfiguration() {
        return new MyGC(g.getDeviceConfiguration());
    }

    @Override
    public void addRenderingHints(Map<?, ?> arg0) {
        g.addRenderingHints(arg0);
    }

    @Override
    public void clearRect(int arg0, int arg1, int arg2, int arg3) {
        g.clearRect(arg0, arg1, arg2, arg3);
    }

    @Override
    public void clip(Shape arg0) {
        g.clip(arg0);
    }

    @Override
    public void clipRect(int arg0, int arg1, int arg2, int arg3) {
        g.clipRect(arg0, arg1, arg2, arg3);
    }

    @Override
    public void copyArea(int arg0, int arg1, int arg2, int arg3, int arg4,
            int arg5) {
        g.copyArea(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public void dispose() {
        g.dispose();
    }

    @Override
    public void draw(Shape arg0) {
        g.draw(arg0);
    }

    @Override
    public void draw3DRect(int x, int y, int width, int height, boolean raised) {
        g.draw3DRect(x, y, width, height, raised);
    }

    @Override
    public void drawArc(int arg0, int arg1, int arg2, int arg3, int arg4,
            int arg5) {
        g.drawArc(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public void drawBytes(byte[] data, int offset, int length, int x, int y) {
        g.drawBytes(data, offset, length, x, y);
    }

    @Override
    public void drawChars(char[] data, int offset, int length, int x, int y) {
        g.drawChars(data, offset, length, x, y);
    }

    @Override
    public void drawGlyphVector(GlyphVector arg0, float arg1, float arg2) {
        g.drawGlyphVector(arg0, arg1, arg2);
    }

    @Override
    public void drawImage(BufferedImage arg0, BufferedImageOp arg1, int arg2,
            int arg3) {
        g.drawImage(arg0, arg1, arg2, arg3);
    }

    @Override
    public boolean drawImage(Image arg0, AffineTransform arg1,
            ImageObserver arg2) {
        return g.drawImage(arg0, arg1, arg2);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, Color arg3,
            ImageObserver arg4) {
        return g.drawImage(arg0, arg1, arg2, arg3, arg4);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, ImageObserver arg3) {
        return g.drawImage(arg0, arg1, arg2, arg3);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, int arg3,
            int arg4, Color arg5, ImageObserver arg6) {
        return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, int arg3,
            int arg4, ImageObserver arg5) {
        return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, int arg3,
            int arg4, int arg5, int arg6, int arg7, int arg8, Color arg9,
            ImageObserver arg10) {
        return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
                arg8, arg9, arg10);
    }

    @Override
    public boolean drawImage(Image arg0, int arg1, int arg2, int arg3,
            int arg4, int arg5, int arg6, int arg7, int arg8, ImageObserver arg9) {
        return g.drawImage(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
                arg8, arg9);
    }

    @Override
    public void drawLine(int arg0, int arg1, int arg2, int arg3) {
        g.drawLine(arg0, arg1, arg2, arg3);
    }

    @Override
    public void drawOval(int arg0, int arg1, int arg2, int arg3) {
        g.drawOval(arg0, arg1, arg2, arg3);
    }

    @Override
    public void drawPolygon(int[] arg0, int[] arg1, int arg2) {
        g.drawPolygon(arg0, arg1, arg2);
    }

    @Override
    public void drawPolygon(Polygon p) {
        g.drawPolygon(p);
    }

    @Override
    public void drawPolyline(int[] arg0, int[] arg1, int arg2) {
        g.drawPolyline(arg0, arg1, arg2);
    }

    @Override
    public void drawRect(int x, int y, int width, int height) {
        g.drawRect(x, y, width, height);
    }

    @Override
    public void drawRenderableImage(RenderableImage arg0, AffineTransform arg1) {
        g.drawRenderableImage(arg0, arg1);
    }

    @Override
    public void drawRenderedImage(RenderedImage arg0, AffineTransform arg1) {
        g.drawRenderedImage(arg0, arg1);
    }

    @Override
    public void drawRoundRect(int arg0, int arg1, int arg2, int arg3, int arg4,
            int arg5) {
        g.drawRoundRect(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public void drawString(AttributedCharacterIterator arg0, float arg1,
            float arg2) {
        g.drawString(arg0, arg1, arg2);
    }

    @Override
    public void drawString(AttributedCharacterIterator arg0, int arg1, int arg2) {
        g.drawString(arg0, arg1, arg2);
    }

    @Override
    public void drawString(String arg0, float arg1, float arg2) {
        g.drawString(arg0, arg1, arg2);
    }

    @Override
    public void drawString(String arg0, int arg1, int arg2) {
        g.drawString(arg0, arg1, arg2);
    }

    @Override
    public boolean equals(Object obj) {
        return g.equals(obj);
    }

    @Override
    public void fill(Shape arg0) {
        g.fill(arg0);
    }

    @Override
    public void fill3DRect(int x, int y, int width, int height, boolean raised) {
        g.fill3DRect(x, y, width, height, raised);
    }

    @Override
    public void fillArc(int arg0, int arg1, int arg2, int arg3, int arg4,
            int arg5) {
        g.fillArc(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public void fillOval(int arg0, int arg1, int arg2, int arg3) {
        g.fillOval(arg0, arg1, arg2, arg3);
    }

    @Override
    public void fillPolygon(int[] arg0, int[] arg1, int arg2) {
        g.fillPolygon(arg0, arg1, arg2);
    }

    @Override
    public void fillPolygon(Polygon p) {
        g.fillPolygon(p);
    }

    @Override
    public void fillRect(int arg0, int arg1, int arg2, int arg3) {
        g.fillRect(arg0, arg1, arg2, arg3);
    }

    @Override
    public void fillRoundRect(int arg0, int arg1, int arg2, int arg3, int arg4,
            int arg5) {
        g.fillRoundRect(arg0, arg1, arg2, arg3, arg4, arg5);
    }

    @Override
    public void finalize() {
        g.finalize();
    }

    @Override
    public Color getBackground() {
        return g.getBackground();
    }

    @Override
    public Shape getClip() {
        return g.getClip();
    }

    @Override
    public Rectangle getClipBounds() {
        return g.getClipBounds();
    }

    @Override
    public Rectangle getClipBounds(Rectangle r) {
        return g.getClipBounds(r);
    }

    @Override
    @SuppressWarnings("deprecation")
    public Rectangle getClipRect() {
        return g.getClipRect();
    }

    @Override
    public Color getColor() {
        return g.getColor();
    }

    @Override
    public Composite getComposite() {
        return g.getComposite();
    }

    @Override
    public Font getFont() {
        return g.getFont();
    }

    @Override
    public FontMetrics getFontMetrics() {
        return g.getFontMetrics();
    }

    @Override
    public FontMetrics getFontMetrics(Font arg0) {
        return g.getFontMetrics(arg0);
    }

    @Override
    public FontRenderContext getFontRenderContext() {
        return g.getFontRenderContext();
    }

    @Override
    public Paint getPaint() {
        return g.getPaint();
    }

    @Override
    public Object getRenderingHint(Key arg0) {
        return g.getRenderingHint(arg0);
    }

    @Override
    public RenderingHints getRenderingHints() {
        return g.getRenderingHints();
    }

    @Override
    public Stroke getStroke() {
        return g.getStroke();
    }

    @Override
    public AffineTransform getTransform() {
        return g.getTransform();
    }

    @Override
    public int hashCode() {
        return g.hashCode();
    }

    @Override
    public boolean hit(Rectangle arg0, Shape arg1, boolean arg2) {
        return g.hit(arg0, arg1, arg2);
    }

    @Override
    public boolean hitClip(int x, int y, int width, int height) {
        return g.hitClip(x, y, width, height);
    }

    @Override
    public void rotate(double arg0, double arg1, double arg2) {
        g.rotate(arg0, arg1, arg2);
    }

    @Override
    public void rotate(double arg0) {
        g.rotate(arg0);
    }

    @Override
    public void scale(double arg0, double arg1) {
        g.scale(arg0, arg1);
    }

    @Override
    public void setBackground(Color arg0) {
        g.setBackground(arg0);
    }

    @Override
    public void setClip(int arg0, int arg1, int arg2, int arg3) {
        g.setClip(arg0, arg1, arg2, arg3);
    }

    @Override
    public void setClip(Shape arg0) {
        g.setClip(arg0);
    }

    @Override
    public void setColor(Color arg0) {
        g.setColor(arg0);
    }

    @Override
    public void setComposite(Composite arg0) {
        g.setComposite(arg0);
    }

    @Override
    public void setFont(Font arg0) {
        g.setFont(arg0);
    }

    @Override
    public void setPaint(Paint arg0) {
        g.setPaint(arg0);
    }

    @Override
    public void setPaintMode() {
        g.setPaintMode();
    }

    @Override
    public void setRenderingHint(Key arg0, Object arg1) {
        g.setRenderingHint(arg0, arg1);
    }

    @Override
    public void setRenderingHints(Map<?, ?> arg0) {
        g.setRenderingHints(arg0);
    }

    @Override
    public void setStroke(Stroke arg0) {
        g.setStroke(arg0);
    }

    @Override
    public void setTransform(AffineTransform arg0) {
        g.setTransform(arg0);
    }

    @Override
    public void setXORMode(Color arg0) {
        g.setXORMode(arg0);
    }

    @Override
    public void shear(double arg0, double arg1) {
        g.shear(arg0, arg1);
    }

    @Override
    public String toString() {
        return g.toString();
    }

    @Override
    public void transform(AffineTransform arg0) {
        g.transform(arg0);
    }

    @Override
    public void translate(double arg0, double arg1) {
        g.translate(arg0, arg1);
    }

    @Override
    public void translate(int arg0, int arg1) {
        g.translate(arg0, arg1);
    }

}

/**
 * Appleの 印刷用 GraphicsConfigurationのバグを修正するクラス。 createCompatibleImage のみ動作が異なる。
 * 他はコンストラクタでセットしたオブジェクトに委譲する。
 * 
 * @author keigoi
 */
class MyGC extends GraphicsConfiguration {
    GraphicsConfiguration gc;

    public MyGC(GraphicsConfiguration gc) {
        this.gc = gc;
    }

    /**
     * Appleの印刷用 createCompatibleImage はいつも nullを返すので、そのかわりに BufferedImage を返す
     */
    @Override
    public BufferedImage createCompatibleImage(int width, int height,
            int transparency) {
        ColorModel cm = gc.getColorModel(transparency);
        WritableRaster raster = cm
                .createCompatibleWritableRaster(width, height);
        return new BufferedImage(cm, raster, false,
                new Hashtable<Object, Object>());
    }

    /**
     * Appleの印刷用 createCompatibleImage はいつも nullを返すので、そのかわりに BufferedImage を返す
     */
    @Override
    public BufferedImage createCompatibleImage(int width, int height) {
        ColorModel cm = gc.getColorModel();
        WritableRaster raster = cm
                .createCompatibleWritableRaster(width, height);
        return new BufferedImage(cm, raster, false,
                new Hashtable<Object, Object>());
    }

    @Override
    public VolatileImage createCompatibleVolatileImage(int width, int height,
            ImageCapabilities caps, int transparency) throws AWTException {
        return gc.createCompatibleVolatileImage(width, height, caps,
                transparency);
    }

    @Override
    public VolatileImage createCompatibleVolatileImage(int width, int height,
            ImageCapabilities caps) throws AWTException {
        return gc.createCompatibleVolatileImage(width, height, caps);
    }

    @Override
    public VolatileImage createCompatibleVolatileImage(int width, int height,
            int transparency) {
        return gc.createCompatibleVolatileImage(width, height, transparency);
    }

    @Override
    public VolatileImage createCompatibleVolatileImage(int width, int height) {
        return gc.createCompatibleVolatileImage(width, height);
    }

    @Override
    public boolean equals(Object obj) {
        return gc.equals(obj);
    }

    @Override
    public Rectangle getBounds() {
        return gc.getBounds();
    }

    @Override
    public BufferCapabilities getBufferCapabilities() {
        return gc.getBufferCapabilities();
    }

    @Override
    public ColorModel getColorModel() {
        return gc.getColorModel();
    }

    @Override
    public ColorModel getColorModel(int transparency) {
        return gc.getColorModel(transparency);
    }

    @Override
    public AffineTransform getDefaultTransform() {
        return gc.getDefaultTransform();
    }

    @Override
    public GraphicsDevice getDevice() {
        return gc.getDevice();
    }

    @Override
    public ImageCapabilities getImageCapabilities() {
        return gc.getImageCapabilities();
    }

    @Override
    public AffineTransform getNormalizingTransform() {
        return gc.getNormalizingTransform();
    }

    @Override
    public int hashCode() {
        return gc.hashCode();
    }

    @Override
    public String toString() {
        return gc.toString();
    }
}