如何将Java 2D Shape对象序列化为XML?

时间:2014-10-27 00:52:18

标签: java xml swing awt java-2d

Shape接口由Java 2D(Arc2DAreaCubicCurve2DEllipse2DGeneralPath等对象实现。 )。

某些具体对象标记为Serializable,可以使用对象序列化进行存储和恢复,但Area之类的其他对象不实现接口并抛出错误。

但是,由于我们经常被警告说这种天真的序列化在Java实现或版本中不一定是稳定的,我更喜欢使用某种形式的序列化。

这导致我们使用XMLEncoderXMLDecoder从XML存储/恢复,但是它能够处理更少的Java 2D Shape对象。

两者的一些结果可以在下面看到。我们从6个形状开始,尝试通过对象序列化和标准XML序列化来存储/恢复它们。

enter image description here

我们如何通过XML正确存储所有Shape个对象?

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.TitledBorder;

public class Serialize2D {

    private JPanel ui;

    Serialize2D() {
        initUI();
    }

    public void initUI() {
        if (ui != null) {
            return;
        }
        ui = new JPanel(new GridLayout(0, 1));

        int[] xpoints = {205, 295, 205, 295};
        int[] ypoints = {5, 25, 25, 45};
        Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length);

        ArrayList<Shape> shapes = new ArrayList<Shape>();
        int w = 45;
        shapes.add(new Rectangle2D.Double(5, 5, 90, 40));
        shapes.add(new Ellipse2D.Double(105, 5, 90, 40));
        shapes.add(polygon);
        shapes.add(new GeneralPath(new Rectangle2D.Double(5, 55, 90, 40)));
        shapes.add(new Path2D.Double(new Rectangle2D.Double(105, 55, 90, 40)));
        shapes.add(new Area(new Rectangle2D.Double(205, 55, 90, 40)));

        addTitledLabelToPanel(shapes, "Original Shapes");
        addTitledLabelToPanel(
                serializeToFromObject(shapes), "Serialize via Object");
        addTitledLabelToPanel(
                serializeToFromXML(shapes), "Serialize via XML");
    }

    public JComponent getUI() {
        return ui;
    }

    public ArrayList<Shape> serializeToFromObject(ArrayList<Shape> shapes) {
        ArrayList<Shape> shps = new ArrayList<Shape>();
        try {
            ObjectOutputStream oos = null;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            for (Shape shape : shapes) {
                try {
                    oos.writeObject(shape);
                } catch (Exception ex) {
                    System.err.println(ex.toString());
                }
            }
            oos.flush();
            oos.close();
            System.out.println("length Obj: " + baos.toByteArray().length);
            ByteArrayInputStream bais = new ByteArrayInputStream(
                    baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);

            Object o = null;
            try {
                o = ois.readObject();
            } catch (NotSerializableException ex) {
                System.err.println(ex.getMessage());
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }
            while (o != null) {
                shps.add((Shape) o);
                try {
                    o = ois.readObject();
                } catch (NotSerializableException ex) {
                    System.err.println(ex.getMessage());
                } catch (ClassNotFoundException ex) {
                    ex.printStackTrace();
                }
            }
            return shps;
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return shps;
    }

    public ArrayList<Shape> serializeToFromXML(ArrayList<Shape> shapes) {
        ArrayList<Shape> shps = new ArrayList<Shape>();
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            XMLEncoder xmle = new XMLEncoder(baos);
            for (Shape shape : shapes) {
                xmle.writeObject(shape);
            }
            xmle.flush();
            xmle.close();

            System.out.println("length XML: " + baos.toByteArray().length);
            ByteArrayInputStream bais
                    = new ByteArrayInputStream(baos.toByteArray());
            XMLDecoder xmld = new XMLDecoder(bais);
            Shape shape = (Shape) xmld.readObject();
            while (shape != null) {
                shps.add(shape);
                try {
                    shape = (Shape) xmld.readObject();
                } catch (ArrayIndexOutOfBoundsException aioobe) {
                    // we've read last object
                    shape = null;
                }
            }
            xmld.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return shps;
    }

    private final static String getType(Object o) {
        String s = o.getClass().getName();
        String[] parts = s.split("\\.");
        s = parts[parts.length - 1].split("\\$")[0];
        return s;
    }

    public static void drawShapesToImage(
            ArrayList<Shape> shapes, BufferedImage image) {
        Graphics2D g = image.createGraphics();
        g.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        g.setColor(Color.WHITE);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
        for (Shape shape : shapes) {
            String s = getType(shape);
            g.setColor(Color.GREEN);
            g.fill(shape);
            g.setColor(Color.BLACK);
            g.draw(shape);
            Rectangle r = shape.getBounds();
            int x = r.x + 5;
            int y = r.y + 16;
            if (r.width * r.height != 0) {
                g.drawString(s, x, y);
            }
        }

        g.dispose();
    }

    private void addTitledLabelToPanel(ArrayList<Shape> shapes, String title) {
        int w = 300;
        int h = 100;
        BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        drawShapesToImage(shapes, bi);
        JLabel l = new JLabel(new ImageIcon(bi));
        l.setBorder(new TitledBorder(title));
        ui.add(l);
    }

    public static void main(String[] args) {
        Runnable r = new Runnable() {

            @Override
            public void run() {
                Serialize2D ss = new Serialize2D();
                JOptionPane.showMessageDialog(null, ss.getUI());
            }
        };
        SwingUtilities.invokeLater(r);
    }
}

2 个答案:

答案 0 :(得分:10)

不幸的是,使用Shape / XMLEncoderDecoder到XML的天真编码/解码通常会破坏Shape的所有重要信息!

为了做到这一点,仍然使用上面提到的类,我们序列化并恢复正确构造的bean,它们表示从 PathIterator获得的形状部分。这些bean是:

  • PathBean,它存储形成Java-2D PathSegment形状的Shape个对象的集合。
  • PathSegment存储路径特定部分的细节(段类型,缠绕规则和协调)。

SerializeShapes GUI

用于演示存储和恢复形状的GUI。

  • 点击 Ellipse Ellipse2D), Rectangle Rectangle2D)或 Face ({{1} })按钮几次。
  • 退出GUI。形状将被序列化为磁盘。
  • 重新启动GUI。上次随机绘制的形状将从磁盘和磁盘恢复。重新出现在GUI中。

enter image description here

所选形状将以绿色填充,其他形状将以红色填充。

Area

package serialize2d; import java.awt.*; import java.awt.event.*; import java.awt.font.FontRenderContext; import java.awt.geom.AffineTransform; import java.awt.geom.*; import java.awt.image.BufferedImage; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Enumeration; import java.util.Random; import java.util.Vector; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.*; /** A GUI to make it easy to add/remove shapes from a canvas. It should persist the shapes between runs. */ public class SerializeShapes { JPanel ui; JPanel shapePanel; Random rand; JPanel shapeCanvas; DefaultListModel<Shape> allShapesModel; ListSelectionModel shapeSelectionModel; RenderingHints renderingHints; SerializeShapes() { initUI(); } public void initUI() { if (ui != null) { return; } renderingHints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); renderingHints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); renderingHints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); ui = new JPanel(new BorderLayout(4, 4)); ui.setBorder(new EmptyBorder(4, 4, 4, 4)); JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4)); ui.add(controls, BorderLayout.PAGE_START); shapeCanvas = new ShapeCanvas(); ui.add(shapeCanvas); rand = new Random(); allShapesModel = new DefaultListModel<Shape>(); JList<Shape> allShapes = new JList<Shape>(allShapesModel); allShapes.setCellRenderer(new ShapeListCellRenderer()); shapeSelectionModel = allShapes.getSelectionModel(); shapeSelectionModel.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); ListSelectionListener shapesSelectionListener = new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { shapeCanvas.repaint(); } }; allShapes.addListSelectionListener(shapesSelectionListener); JScrollPane shapesScroll = new JScrollPane( allShapes, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); // TODO fix this hack.. shapesScroll.getViewport().setPreferredSize(new Dimension(60, 200)); ui.add(shapesScroll, BorderLayout.LINE_START); Action addEllipse = new AbstractAction("Ellipse") { @Override public void actionPerformed(ActionEvent e) { int w = rand.nextInt(100) + 10; int h = rand.nextInt(100) + 10; int x = rand.nextInt(shapeCanvas.getWidth() - w); int y = rand.nextInt(shapeCanvas.getHeight() - h); Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h); addShape(ellipse); } }; addEllipse.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E); Action addRectangle = new AbstractAction("Rectangle") { @Override public void actionPerformed(ActionEvent e) { int w = rand.nextInt(100) + 10; int h = rand.nextInt(100) + 10; int x = rand.nextInt(shapeCanvas.getWidth() - w); int y = rand.nextInt(shapeCanvas.getHeight() - h); Rectangle2D rectangle = new Rectangle2D.Double(x, y, w, h); addShape(rectangle); } }; addRectangle.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R); final int faceStart = 128513; final int faceEnd = 128528; final int diff = faceEnd - faceStart; StringBuilder sb = new StringBuilder(); for (int count = faceStart; count <= faceEnd; count++) { sb.append(Character.toChars(count)); } final String s = sb.toString(); Vector<Font> compatibleFontList = new Vector<Font>(); GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); Font[] fonts = ge.getAllFonts(); for (Font font : fonts) { if (font.canDisplayUpTo(s) < 0) { compatibleFontList.add(font); } } JComboBox fontChooser = new JComboBox(compatibleFontList); ListCellRenderer fontRenderer = new DefaultListCellRenderer() { @Override public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component c = super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus); JLabel l = (JLabel) c; Font font = (Font) value; l.setText(font.getName()); return l; } }; fontChooser.setRenderer(fontRenderer); final ComboBoxModel<Font> fontModel = fontChooser.getModel(); BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); Graphics2D g = bi.createGraphics(); final FontRenderContext fontRenderContext = g.getFontRenderContext(); Action addFace = new AbstractAction("Face") { @Override public void actionPerformed(ActionEvent e) { int codepoint = faceStart + rand.nextInt(diff); String text = new String(Character.toChars(codepoint)); Font font = (Font) fontModel.getSelectedItem(); Area area = new Area( font.deriveFont(80f). createGlyphVector(fontRenderContext, text). getOutline()); Rectangle bounds = area.getBounds(); float x = rand.nextInt( shapeCanvas.getWidth() - bounds.width) - bounds.x; float y = rand.nextInt( shapeCanvas.getHeight() - bounds.height) - bounds.y; AffineTransform move = AffineTransform. getTranslateInstance(x, y); area.transform(move); addShape(area); } }; addFace.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F); Action delete = new AbstractAction("Delete") { @Override public void actionPerformed(ActionEvent e) { int idx = shapeSelectionModel.getMinSelectionIndex(); if (idx < 0) { JOptionPane.showMessageDialog( ui, "Select a shape to delete", "Select a Shape", JOptionPane.ERROR_MESSAGE); } else { allShapesModel.removeElementAt(idx); shapeCanvas.repaint(); } } }; delete.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D); controls.add(new JButton(addEllipse)); controls.add(new JButton(addRectangle)); controls.add(new JButton(addFace)); controls.add(fontChooser); controls.add(new JButton(delete)); try { ArrayList<Shape> shapes = deserializeShapes(); for (Shape shape : shapes) { allShapesModel.addElement(shape); } } catch (Exception ex) { System.err.println("If first launch, this is as expected!"); ex.printStackTrace(); } } private void addShape(Shape shape) { allShapesModel.addElement(shape); int size = allShapesModel.getSize() - 1; shapeSelectionModel.addSelectionInterval(size, size); } class ShapeCanvas extends JPanel { ShapeCanvas() { setBackground(Color.WHITE); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHints(renderingHints); Stroke stroke = new BasicStroke(1.5f); g2.setStroke(stroke); int idx = shapeSelectionModel.getMinSelectionIndex(); Shape selectedShape = null; if (idx > -1) { selectedShape = allShapesModel.get(idx); } Enumeration en = allShapesModel.elements(); while (en.hasMoreElements()) { Shape shape = (Shape) en.nextElement(); if (shape.equals(selectedShape)) { g2.setColor(new Color(0, 255, 0, 191)); } else { g2.setColor(new Color(255, 0, 0, 191)); } g2.fill(shape); g2.setColor(new Color(0, 0, 0, 224)); g2.draw(shape); } } @Override public Dimension getPreferredSize() { return new Dimension(500, 300); } } public JComponent getUI() { return ui; } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { SerializeShapes se = new SerializeShapes(); JFrame f = new JFrame("Serialize Shapes"); f.addWindowListener(new SerializeWindowListener(se)); f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); f.setContentPane(se.getUI()); f.setResizable(false); f.pack(); f.setLocationByPlatform(true); f.setVisible(true); } }; SwingUtilities.invokeLater(r); } public void serializeShapes() throws FileNotFoundException { ArrayList<Shape> shapes = new ArrayList<Shape>(); Enumeration en = allShapesModel.elements(); while (en.hasMoreElements()) { Shape shape = (Shape) en.nextElement(); shapes.add(shape); } ShapeIO.serializeShapes(shapes, this.getClass()); try { Desktop.getDesktop().open( ShapeIO.getSerializeFile(this.getClass())); } catch (Exception e) { e.printStackTrace(); } } public ArrayList<Shape> deserializeShapes() throws FileNotFoundException { return ShapeIO.deserializeShapes(this.getClass()); } class ShapeListCellRenderer extends DefaultListCellRenderer { @Override public Component getListCellRendererComponent( JList<? extends Object> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); JLabel l = (JLabel) c; Shape shape = (Shape) value; ShapeIcon icon = new ShapeIcon(shape, 40); l.setIcon(icon); l.setText(""); return l; } } class ShapeIcon implements Icon { Shape shape; int size; ShapeIcon(Shape shape, int size) { this.shape = shape; this.size = size; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHints(renderingHints); Rectangle bounds = shape.getBounds(); int xOff = -bounds.x; int yOff = -bounds.y; double xRatio = (double) bounds.width / (double) size; double yRatio = (double) bounds.height / (double) size; double ratio = xRatio > yRatio ? xRatio : yRatio; AffineTransform scale = AffineTransform.getScaleInstance(1 / ratio, 1 / ratio); AffineTransform shift = AffineTransform.getTranslateInstance(xOff, yOff); AffineTransform totalTransform = new AffineTransform(); totalTransform.concatenate(scale); totalTransform.concatenate(shift); Area b = new Area(shape).createTransformedArea(totalTransform); bounds = b.getBounds(); g2.setColor(Color.BLACK); g2.fill(b); } @Override public int getIconWidth() { return size; } @Override public int getIconHeight() { return size; } } } class SerializeWindowListener extends WindowAdapter { SerializeShapes serializeShapes; SerializeWindowListener(SerializeShapes serializeShapes) { this.serializeShapes = serializeShapes; } @Override public void windowClosing(WindowEvent e) { try { serializeShapes.serializeShapes(); } catch (FileNotFoundException ex) { ex.printStackTrace(); System.exit(1); } System.exit(0); } }

执行XML的I / O.

ShapeIO

package serialize2d; import java.awt.Shape; import java.beans.*; import java.io.*; import java.util.ArrayList; public class ShapeIO { /** Save the list of shapes to the file system. */ public static void serializeShapes( ArrayList<Shape> shapes, Class serializeClass) throws FileNotFoundException { File f = getSerializeFile(serializeClass); XMLEncoder xmle = new XMLEncoder(new FileOutputStream(f)); ArrayList<PathBean> pathSegmentsCollection = new ArrayList<>(); for (Shape shape : shapes) { ArrayList<PathSegment> pathSegments = BeanConverter.getSegmentsFromShape(shape); PathBean as = new PathBean(pathSegments); pathSegmentsCollection.add(as); } xmle.writeObject(pathSegmentsCollection); xmle.flush(); xmle.close(); } /** Load the list of shapes from the file system. */ public static ArrayList<Shape> deserializeShapes(Class serializeClass) throws FileNotFoundException { File f = getSerializeFile(serializeClass); XMLDecoder xmld = new XMLDecoder(new FileInputStream(f)); ArrayList<PathBean> pathSegmentsCollection = (ArrayList<PathBean>) xmld.readObject(); ArrayList<Shape> shapes = new ArrayList<Shape>(); for (PathBean pathSegments : pathSegmentsCollection) { shapes.add(BeanConverter.getShapeFromSegments(pathSegments)); } return shapes; } /** Provide an unique, reproducible & readable/writable path for a class. */ public static File getSerializeFile(Class serializeClass) { File f = new File(System.getProperty("user.home")); String[] nameParts = serializeClass.getCanonicalName().split("\\."); f = new File(f, "java"); for (String namePart : nameParts) { f = new File(f, namePart); } f.mkdirs(); f = new File(f, nameParts[nameParts.length-1] + ".xml"); return f; } }

BeanConverter获取PathIterator并将其转换为可序列化的bean。将bean转换回Shape

GeneralPath

package serialize2d; import java.awt.Shape; import java.awt.geom.*; import java.util.ArrayList; /** Utility class to convert bean to/from a Shape. */ public class BeanConverter { /** Convert a shape to a serializable bean. */ public static ArrayList<PathSegment> getSegmentsFromShape(Shape shape) { ArrayList<PathSegment> shapeSegments = new ArrayList<PathSegment>(); for ( PathIterator pi = shape.getPathIterator(null); !pi.isDone(); pi.next()) { double[] coords = new double[6]; int pathSegmentType = pi.currentSegment(coords); int windingRule = pi.getWindingRule(); PathSegment as = new PathSegment( pathSegmentType, windingRule, coords); shapeSegments.add(as); } return shapeSegments; } /** Convert a serializable bean to a shape. */ public static Shape getShapeFromSegments(PathBean shapeSegments) { GeneralPath gp = new GeneralPath(); for (PathSegment shapeSegment : shapeSegments.getPathSegments()) { double[] coords = shapeSegment.getCoords(); int pathSegmentType = shapeSegment.getPathSegmentType(); int windingRule = shapeSegment.getWindingRule(); gp.setWindingRule(windingRule); if (pathSegmentType == PathIterator.SEG_MOVETO) { gp.moveTo(coords[0], coords[1]); } else if (pathSegmentType == PathIterator.SEG_LINETO) { gp.lineTo(coords[0], coords[1]); } else if (pathSegmentType == PathIterator.SEG_QUADTO) { gp.quadTo(coords[0], coords[1], coords[2], coords[3]); } else if (pathSegmentType == PathIterator.SEG_CUBICTO) { gp.curveTo( coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); } else if (pathSegmentType == PathIterator.SEG_CLOSE) { gp.closePath(); } else { System.err.println("Unexpected value! " + pathSegmentType); } } return gp; } }

在可串行化的bean中存储路径段的集合。

PathBean

package serialize2d; import java.awt.geom.*; import java.util.ArrayList; /** PathBean stores the collection of PathSegment objects that constitute the path of a Shape. */ public class PathBean { public ArrayList<PathSegment> pathSegments; public PathBean() {} public PathBean(ArrayList<PathSegment> pathSegments) { this.pathSegments = pathSegments; } public ArrayList<PathSegment> getPathSegments() { return pathSegments; } public void setPathSegments(ArrayList<PathSegment> pathSegments) { this.pathSegments = pathSegments; } @Override public String toString() { StringBuilder sb = new StringBuilder("{"); for (PathSegment pathSegment : pathSegments) { sb.append(" \n\t"); sb.append(pathSegment.toString()); } sb.append(" \n"); sb.append("}"); return "PathSegments: " + sb.toString(); } }

存储整个路径的一部分的路径段。

PathSegment

备注

这旨在作为概念证明,而不是抛光方法。

  • XML序列化数据变得非常快,通常会被压缩。 Zip压缩可能会使序列化对象或类文件的字节大小减少30-40%,但XML减少80-95%。在任何情况下,zip也适用于下一点。
  • 对于我们希望提供序列化和恢复形状的项目类型,我们可能 想要包括形状的更多细节(例如填充颜色或纹理以及绘制颜色或笔划等)以及其他数据(如图像或字体)。这也是Zip派上用场的地方,因为我们可以将它们全部放在同一个档案中,每个档案都有最佳的压缩级别(例如XML标准和图像无标准)。

可以从我的云端硬盘下载zip archive of the source files in this answer

答案 1 :(得分:1)

自定义PersistenceDelegate可与XMLEncoder一起使用,以将Path2DGeneralPath序列化为XML。

考虑以下XML:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
 <object class="java.awt.geom.Path2D$Float">
  <void property="windingRule">
   <int>0</int>
  </void>
  <void method="moveTo">
   <float>1.0</float>
   <float>1.0</float>
  </void>
  <void method="lineTo">
   <float>2.0</float>
   <float>0.0</float>
  </void>
  <void method="lineTo">
   <float>0.0</float>
   <float>3.0</float>
  </void>
  <void method="closePath"/>
 </object>
</java>

XMLEncoder实例读取时,将执行以下命令...

Path2D.Float object = new Path2D.Float();
object.setWindingRule(0); // Note: 0 => Path2D.WIND_EVEN_ODD
object.moveTo(1.0, 1.0);
object.lineTo(2.0, 0.0);
object.lineTo(0.0, 3.0);
object.closePath();

... XMLDecoder.readObject()将返回一个封闭的三角形对象。

基于此,我们可以得出结论:XMLDecoder已经可以对Path2D形状进行反序列化(如果它已正确编码)。 XMLEncoder现在为我们做了什么?

Path2D.Float path = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 10);
path.moveTo(1, 1);
path.lineTo(2, 0);
path.lineTo(0, 3);
path.closePath();

try (XMLEncoder xml = new XMLEncoder(System.out)) {
    xml.writeObject(path);
}

这会生成以下XML:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
 <object class="java.awt.geom.Path2D$Float">
  <void property="windingRule">
   <int>0</int>
  </void>
 </object>
</java>

不是很好,但也不是太糟糕。我们只是错过了路径数据。所以我们只需要扩展DefaultPersistenceDelegate以将所需的路径命令添加到输出中。

public class Path2DPersistenceDelegate extends DefaultPersistenceDelegate {

    @Override
    protected void initialize(Class<?> cls, Object oldInstance, Object newInstance, Encoder out) {
        super.initialize(cls, oldInstance, newInstance, out);

        Shape shape = (Shape) oldInstance;

        float coords[] = new float[6];
        Float pnt0[] = new Float[0];
        Float pnt1[] = new Float[2];
        Float pnt2[] = new Float[4];
        Float pnt3[] = new Float[6];
        Float pnts[];

        PathIterator iterator = shape.getPathIterator(null);
        while (!iterator.isDone()) {
            int type = iterator.currentSegment(coords);
            String cmd;
            switch (type) {
            case PathIterator.SEG_CLOSE:
                cmd = "closePath";
                pnts = pnt0;
                break;
            case PathIterator.SEG_MOVETO:
                cmd = "moveTo";
                pnts = pnt1;
                break;
            case PathIterator.SEG_LINETO:
                cmd = "lineTo";
                pnts = pnt1;
                break;
            case PathIterator.SEG_QUADTO:
                cmd = "quadTo";
                pnts = pnt2;
                break;
            case PathIterator.SEG_CUBICTO:
                cmd = "curveTo";
                pnts = pnt3;
                break;
            default:
                throw new IllegalStateException("Unexpected segment type: " + type);
            }

            for (int i = 0; i < pnts.length; i++) {
                pnts[i] = coords[i];
            }

            out.writeStatement(new Statement(oldInstance, cmd, pnts));
            iterator.next();
        }
    }
}

然后,我们只使用XMLEncoder注册此持久性委托,它将生成此帖子顶部显示的XML。

Path2DPersistenceDelegate path2d_delegate = new Path2DPersistenceDelegate();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
    xml.setPersistenceDelegate(Path2D.Float.class, path2d_delegate);
    xml.writeObject(path);
}

由于Path2D.FloatGeneralPath的父类,因此GeneralPath也会被正确编码。如果要正确编码Path2D.Double形状,则需要修改委托以使用double值和Double个对象。

更新

要使用正确的Path2D.Float属性构造windingRule对象,而不是之后设置属性,请将以下构造函数添加到Path2DPersistenceDelegate

public Path2DPersistenceDelegate() {
    super(new String[] { "windingRule" });
}

然后将读取XML:

...
 <object class="java.awt.geom.Path2D$Float">
  <int>0</int>
  <void method="moveTo">
  ...

这会在XML中丢失一些人类可读的上下文信息;人类需要阅读文档以确定使用Path2D.Float(int)构造函数,int参数是windingRule属性。

更新2:

Polygon持久性委托非常简单:

public class PolygonPersistenceDelegate extends PersistenceDelegate {

    @Override
    protected Expression instantiate(Object oldInstance, Encoder out) {
        Polygon polygon = (Polygon) oldInstance;

        return new Expression(oldInstance, oldInstance.getClass(), "new",
                new Object[] { polygon.xpoints, polygon.ypoints, polygon.npoints });
    }
}

由于Area Constructive Area Geometry对象更复杂,因此无法通过moveTolineTo类型方法创建,而只能通过添加,减去或排除或创建Shape个对象。但构造函数采用Shape对象,Path2D.Double可以从Area对象构造,因此持久代理实际上也可以非常简单地编写:

public class AreaPersistenceDelegate extends PersistenceDelegate {

    @Override
    protected Expression instantiate(Object oldInstance, Encoder out) {
        Area area = (Area) oldInstance;
        Path2D.Double p2d = new Path2D.Double(area);

        return new Expression(oldInstance, oldInstance.getClass(), "new",
                new Object[] { p2d });
    }
}

由于我们在内部使用Path2D.Double,我们需要将两个持久委托添加到XMLEncoder

try (XMLEncoder encoder = new XMLEncoder(baos)) {
    encoder.setPersistenceDelegate(Area.class, new AreaPersistenceDelegate());
    encoder.setPersistenceDelegate(Path2D.Double.class, new Path2DPersistenceDelegate.Double());
    encoder.writeObject(area);
}

更新3:

已在GitHub上创建了PersistenceDelegateAreaPath2D GeneralPath的项目。

注意:

  • 删除了Polygon的持久性委托,因为Java 1.7似乎没有必要

更新4:

对于Java 1.7,必须为每个pnts分配new Statement()数组;它不能分配一次并重复使用。因此,必须按如下方式更改Path2D代理:

        float coords[] = new float[6];
        /* Removed: Float pnt0[] = new Float[0];
                    Float pnt1[] = new Float[0];
                    Float pnt2[] = new Float[4];
                    Float pnt3[] = new Float[6]; */
        Float pnts[];

        PathIterator iterator = shape.getPathIterator(null);
        while (!iterator.isDone()) {
            int type = iterator.currentSegment(coords);
            String cmd;
            switch (type) {
            case PathIterator.SEG_CLOSE:
                cmd = "closePath";
                pnts = new Float[0];  // Allocate for each segment
                break;
            case PathIterator.SEG_MOVETO:
                cmd = "moveTo";
                pnts = new Float[2];  // Allocate for each segment
                break;
            /* ... etc ...*/