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

9

Shape接口由Java 2D (Arc2D, Area, CubicCurve2D, Ellipse2D, GeneralPath等)的对象实现。

其中一些具体对象标记为Serializable,可以使用对象序列化进行存储和恢复,但其他对象(如Area)没有实现该接口并会抛出错误。

但由于我们经常被警告说这种天真的序列化不一定在Java实现或版本之间稳定,因此我更喜欢使用某种形式的序列化来解决这个问题。

这就引导我们使用XMLEncoderXMLDecoder来存储/恢复,但是这两者都无法处理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个回答

10

很不幸,使用XMLEncoder/DecoderShape进行简单编码/解码到XML时,经常会破坏Shape的所有重要信息!

所以为了做到这一点,仍然使用上述类,我们序列化和还原适当构造的bean,该bean表示从PathIterator获得的形状部件。这些豆子是:

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

SerializeShapes GUI

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

  • 点击EllipseEllipse2D),RectangleRectangle2D)或FaceArea)按钮几次。
  • 退出GUI。形状将被序列化到磁盘。
  • 重新启动GUI。上次随机绘制的形状将从磁盘恢复并重新出现在GUI中。

enter image description here

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

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);
    }
}

ShapeIO

ShapeIO模块执行XML文件的输入/输出操作。

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

Shape获取PathIterator并将其转换为可序列化的bean。将bean转换回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;
    }
}

PathBean

存储一组路径段的可序列化bean。

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

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

package serialize2d;

import java.util.Arrays;

/** PathSegment bean stores the detail on one segment of the path
 that constitutes a Shape. */
public class PathSegment {

    public int pathSegmentType;
    public int windingRule;
    public double[] coords;

    public PathSegment() {}

    public PathSegment(int pathSegmentType, int windingRule, double[] coords) {
        this.pathSegmentType = pathSegmentType;
        this.windingRule = windingRule;
        this.coords = coords;
    }

    public int getPathSegmentType() {
        return pathSegmentType;
    }

    public void setPathSegmentType(int pathSegmentType) {
        this.pathSegmentType = pathSegmentType;
    }

    public int getWindingRule() {
        return windingRule;
    }

    public void setWindingRule(int windingRule) {
        this.windingRule = windingRule;
    }

    public double[] getCoords() {
        return coords;
    }

    public void setCoords(double[] coords) {
        this.coords = coords;
    }

    @Override
    public String toString() {
        String sC = (coords != null ? "" : Arrays.toString(coords));
        String s = String.format(
                "PathSegment: Path Segment Type:- %d \t"
                + "Winding Rule:- %d \tcoords:- %s",
                getPathSegmentType(), getWindingRule(), sC);
        return s;
    }
}

注意事项

本文旨在证明概念,而非成熟的方法。

  • XML序列化数据很快就会变得很大,通常需要进行压缩。Zip压缩可以将序列化对象或类文件的字节大小削减30-40%,而对XML则可削减80-95%。无论如何,zip也适用于下一个要点。
  • 对于那种希望提供序列化和还原形状的项目类型,我们可能想包括更多形状的细节(例如填充颜色或纹理、绘制颜色或描边等)以及其他数据,例如图像或字体。这也是Zip派上用场的地方,因为我们可以将它们全部放入同一存档中,每个存档都具有最佳的压缩级别(例如XML的标准级别和图像的无压缩级别)。

可以从我的云驱动器下载此答案中源文件的zip存档


谢谢楼主。顺便问一下,你是在使用JAVA 6吗?还是有其他原因避免使用钻石操作符和try-with-recourse? - Payam
4
@Payam 我喜欢让我的示例代码能够在尽可能低版本的Java上工作。 - Andrew Thompson
此 XML 序列化仅保留对象的“形状”。序列化对象的“类”都被转换为“GeneralPath”对象。只要需要的是形状的内容,这可能是可以接受的。但如果用户希望序列化包含“Area”属性的自定义类,则反序列化将尝试在“Area”属性中存储“GeneralPath”,并且反序列化将失败。 - AJNeufeld

1

可以使用自定义的 PersistenceDelegateXMLEncoder 一起将 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对象。

更新:

为了使用正确的 windingRule 属性构建 Path2D.Float 对象,而不是之后设置属性,将以下构造函数添加到 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 构造区域几何对象较为复杂,因此不能通过 moveTolineTo 类型的方法创建,而只能通过添加、减去或异或 Shape 对象来创建。但是构造函数需要一个 Shape 对象,而可以从 Area 对象构造一个 Path2D.Double,因此持久性代理实际上也可以很简单地编写:

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上创建了一个带有PersistenceDelegate的项目,用于AreaPath2DGeneralPath

注意事项:

  • Polygon的持久化代理已被删除,因为它似乎在Java 1.7中是不必要的。

更新4:

对于Java 1.7,每个new Statement()都必须为pnts数组分配空间;不能只分配一次并重复使用。因此,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 ...*/

好的回答! :) - Andrew Thompson
1
@AndrewThompson 谢谢。能够找到一个我可以做出重要贡献的领域感觉很好。(当然,如果还能得到奖励就更好了。非常感激。)我正在制作一个网站包,其中将收集所有委托的源代码,并放在同一个地方。完成后我会添加一个链接。 - AJNeufeld
“完成后我会添加链接。” 很棒的东西。我期待着它。 “找到一个我可以做出重要贡献的领域,感觉很好。” 有专家在某个主题上提交他们的智慧到我的谦卑问答中,也感觉很好。 - Andrew Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接