Как сериализовать объекты Java 2D Shape как XML?

Интерфейс Shape реализуется объектами Java 2D (Arc2D, Area, CubicCurve2D, Ellipse2D, GeneralPath и т.д.)).

Некоторые из конкретных объектов помечены как Serializable и могут быть сохранены и восстановлены с использованием сериализации объектов, но другие, подобные Area, не реализуют интерфейс и не выбрасывают ошибки.

Но поскольку мы постоянно предупреждаем, что такая наивная сериализация не обязательно стабильна в реализациях или версиях Java, я бы предпочел использовать некоторую форму сериализации.

Это приводит нас к сохранению/восстановлению из XML с помощью XMLEncoder и XMLDecoder, но это способно обрабатывать даже меньше объектов Java 2D Shape.

Некоторые результаты для обоих можно увидеть ниже. Мы начинаем с 6 форм и пытаемся сохранить/восстановить их через сериализацию объектов и стандартную сериализацию XML.

enter image description here

Как мы будем хранить все объекты Shape правильно через XML?

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

Ответ 1

К сожалению, наивное кодирование/декодирование Shape в XML с использованием XMLEncoder/Decoder часто уничтожает всю важную информацию Shape!

Чтобы сделать это, все еще используя вышеупомянутые классы, мы сериализуем и восстанавливаем правильно сконструированные beans, которые представляют части формы, полученные из PathIterator. Эти beans:

  • PathBean, который хранит коллекцию объектов PathSegment, которые формируют форму Java-2D Shape.
  • PathSegment, в котором хранятся детали определенной части пути (тип сегмента, правило обмотки и координаты).

SerializeShapes GUI

Графический интерфейс для демонстрации сохранения и восстановления фигур.

  • Несколько раз нажмите кнопки Ellipse (Ellipse2D), Rectangle (Rectangle2D) или Face (Area).
  • Выход из графического интерфейса. Формы будут сериализованы на диск.
  • Перезагрузите графический интерфейс. Случайно нарисованные фигуры из последнего времени будут восстановлены с диска и снова появятся в графическом интерфейсе.

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

Выполняет ввод/вывод в/из 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

Получает PathIterator из Shape и преобразует его в сериализуемый 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% от размера байта сериализованного объекта или файла класса, но на 80-95% от XML. В любом случае, zip хорошо работает и для следующего пункта.
  • Для типа проекта, где мы хотим предложить сериализацию и восстановление фигур, мы, возможно, также хотим включить более подробную информацию о фигурах (например, цвет заливки или текстуры и рисование цвета или штриха и т.д.), а также другие данные, например изображения или шрифты. Это также полезно для Zip, поскольку мы можем поместить их все в один архив, каждый с лучшими уровнями сжатия (например, стандартный для XML и ни один для изображений).

A zip-архив исходных файлов в этом ответе можно загрузить с моего облачного диска.

Ответ 2

Пользовательский PersistenceDelegate может использоваться с XMLEncoder для сериализации Path2D или GeneralPath в 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.Float является родительским классом GeneralPath, a GeneralPath также будет правильно закодирован. Если вы хотите правильно кодировать фигуры Path2D.Double, вам нужно будет изменить делегат, чтобы использовать значения double и double.


Update:

Чтобы построить объект 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 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 Объект конструктивной области геометрии более сложный, он не может быть создан методами типа moveTo и lineTo, а скорее только добавлением, вычитанием или эксклюзивным или 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:

Проект с PersistenceDelegate для Area, Path2D и GeneralPath был создан на GitHub.

Примечания:

  • Депозит персистентности для 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 ...*/