This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 9fd2e0d706 feat(Geometry): implement WKB Reader and Writer
9fd2e0d706 is described below

commit 9fd2e0d706f72e874b016079ecfe29c80adaa77c
Author: jsorel <[email protected]>
AuthorDate: Wed Sep 16 14:37:37 2026 +0200

    feat(Geometry): implement WKB Reader and Writer
---
 .../main/org/apache/sis/geometries/Geometry.java   |   8 +-
 .../sis/geometries/adapter/WellKnownBinary.java    | 525 +++++++++++++++-
 .../geometries/adapter/WellKnownBinaryParser.java  | 663 +++++++++++++++++++++
 .../geometries/adapter/WellKnownBinaryTest.java    | 414 +++++++++++++
 4 files changed, 1607 insertions(+), 3 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
index 70e5938beb..3997f5d419 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/Geometry.java
@@ -19,6 +19,7 @@ package org.apache.sis.geometries;
 import java.util.List;
 import java.util.Map;
 import javax.measure.quantity.Length;
+import org.apache.sis.geometries.adapter.WellKnownBinary;
 import org.apache.sis.geometries.adapter.WellKnownText;
 import org.apache.sis.geometries.internal.shared.AbstractGeometry;
 import org.apache.sis.geometries.mesh.MeshPrimitive;
@@ -428,15 +429,18 @@ public sealed interface Geometry
      * - this method is located on Encoding sub interface in the standard, it 
is placed
      *   on Geometry to match OGC SFA.</p>
      *
+     * <p>The encoding is delegated to {@link WellKnownBinary}, which 
documents the supported
+     * geometry types and the deviations from the format.</p>
+     *
      * @return this geometry in Well-known Binary
+     * @throws IllegalArgumentException if this geometry has no Well-known 
Binary representation.
      *
      * @see OGC Simple Feature Access 1.2.1 - 6.1.2.2
      * @see ISO 19107:2019 - 6.4.4.3, 6.4.7
      */
     @UML(identifier="asBinary", specification=ISO_19107)
     default byte[] asBinary() {
-        //TODO remove this method default when all classes implement it.
-        throw new UnsupportedOperationException();
+        return new WellKnownBinary().encode(this);
     }
 
     /**
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java
index 50df291e6f..2e393f4730 100644
--- 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinary.java
@@ -16,11 +16,534 @@
  */
 package org.apache.sis.geometries.adapter;
 
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.DataPoints;
+import org.apache.sis.geometries.Empty;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.Orientable;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.curve.CircularString;
+import org.apache.sis.geometries.curve.CompoundCurve;
+import org.apache.sis.geometries.curve.LineString;
+import org.apache.sis.geometries.curve.MultiCurve;
+import org.apache.sis.geometries.curve.MultiLineString;
+import org.apache.sis.geometries.point.MultiPoint;
+import org.apache.sis.geometries.surface.CurvePolygon;
+import org.apache.sis.geometries.surface.MultiPolygon;
+import org.apache.sis.geometries.surface.MultiSurface;
+import org.apache.sis.geometries.surface.Polygon;
+import org.apache.sis.geometries.surface.PolyhedralSurface;
+import org.apache.sis.geometries.surface.TIN;
+import org.apache.sis.geometries.surface.Triangle;
+import org.apache.sis.maths.Tuple;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+
 /**
- * TODO
+ * Encoder and decoder for the Well-Known Binary representation of geometries.
+ *
+ * <p>This is the binary counterpart of {@link WellKnownText}, and covers the 
same geometry types:
+ * those of <cite>OGC Simple Feature Access 1.2.1</cite> extended with the 
curved and surface-patch
+ * types of <cite>ISO 13249-3</cite> (SQL/MM Part 3). Every geometry is a byte 
order flag, a type
+ * code and a body:</p>
+ *
+ * <blockquote><pre>
+ * byte    byteOrder;      // 0 = big endian (XDR), 1 = little endian (NDR)
+ * uint32  type;           // geometry type, plus 1000 for Z, 2000 for M, 3000 
for ZM
+ * …                       // body, which depends on the type
+ * </pre></blockquote>
+ *
+ * <p>The supported type codes, and the geometry type each one maps to, 
are:</p>
+ *
+ * <table class="sis">
+ *   <caption>Supported Well-Known Binary type codes</caption>
+ *   <tr><th>Code</th> <th>Geometry type</th></tr>
+ *   <tr><td>1</td>    <td>{@link Point}</td></tr>
+ *   <tr><td>2</td>    <td>{@link LineString}</td></tr>
+ *   <tr><td>3</td>    <td>{@link Polygon}</td></tr>
+ *   <tr><td>4</td>    <td>{@link MultiPoint}</td></tr>
+ *   <tr><td>5</td>    <td>{@link MultiLineString}</td></tr>
+ *   <tr><td>6</td>    <td>{@link MultiPolygon}</td></tr>
+ *   <tr><td>7</td>    <td>{@link GeometryCollection}</td></tr>
+ *   <tr><td>8</td>    <td>{@link CircularString}</td></tr>
+ *   <tr><td>9</td>    <td>{@link CompoundCurve}</td></tr>
+ *   <tr><td>10</td>   <td>{@link CurvePolygon}</td></tr>
+ *   <tr><td>11</td>   <td>{@link MultiCurve}</td></tr>
+ *   <tr><td>12</td>   <td>{@link MultiSurface}</td></tr>
+ *   <tr><td>15</td>   <td>{@link PolyhedralSurface}</td></tr>
+ *   <tr><td>16</td>   <td>{@link TIN}</td></tr>
+ *   <tr><td>17</td>   <td>{@link Triangle}</td></tr>
+ * </table>
+ *
+ * <p>Any other geometry of the Apache SIS hierarchy
+ * is rejected by {@link #encode encode(…)} with an {@link 
IllegalArgumentException}.</p>
+ *
+ * <h2>Dimensions and measures</h2>
+ * As in {@link WellKnownText}, the {@code Z} flag is the third ordinate of the
+ * {@linkplain AttributesType#ATT_POSITION position} attribute and the {@code 
M} flag is the
+ * separate {@linkplain AttributesType#ATT_M measure} attribute, written as 
the ordinate following
+ * the position ones. A geometry whose position has neither 2 nor 3 dimensions 
cannot be written.
+ * Unlike the text form, the binary form always states the flags, so nothing 
is ever inferred from
+ * the width of a tuple.
+ *
+ * <h2>Deviations</h2>
+ * <ul>
+ *   <li>A {@link org.apache.sis.geometries.curve.LinearRing} is written as a 
{@code LineString}:
+ *       it is a {@code LineString} and Well-Known Binary has no standalone 
ring type. Consequently
+ *       {@code decode(…)} never returns a {@code LinearRing} at the top 
level.</li>
+ *   <li>An {@linkplain Orientable#getOrientationSign() orientation} of
+ *       {@link Orientable.Sign#NEGATIVE} is written as the underlying
+ *       {@linkplain Orientable#getReverse() reverse} primitive. Well-Known 
Binary has no notion
+ *       of orientation, so that information is lost.</li>
+ *   <li>An {@link Empty} geometry is written as an empty {@code 
GeometryCollection}, the only
+ *       type-less empty form the format offers.</li>
+ *   <li>The format has no empty form for a point, since a point is a bare 
coordinate tuple with
+ *       no count in front of it. The widespread convention of writing {@link 
Double#NaN} ordinates
+ *       is followed: an empty point is written that way, and a point whose 
ordinates are all
+ *       {@code NaN} is read as an {@link Empty} geometry rather than as a 
{@code Point}, because
+ *       the model has no empty point. It is therefore written back as an empty
+ *       {@code GeometryCollection}, and an empty point may not appear as a 
member of a
+ *       {@code MultiPoint}. Every other type has a genuine empty form, a 
count of zero, which
+ *       round-trips unchanged.</li>
+ *   <li>The coordinate reference system is neither written nor read: the 
{@code SRID} field of the
+ *       extended Well-Known Binary of some databases is not part of the 
standard, and neither are
+ *       the high order type bits it uses for the dimension flags. Unless a 
system is given to
+ *       {@link #decode(byte[], CoordinateReferenceSystem)}, decoded 
geometries use
+ *       {@link 
org.apache.sis.geometries.Geometries#getUndefinedCRS(int)}.</li>
+ * </ul>
+ *
+ * <h2>Thread safety</h2>
+ * Instances are cheap to create, immutable, and safe for use by multiple 
threads.
  *
  * @author Johann Sorel (Geomatys)
  */
 public final class WellKnownBinary {
+    /**
+     * Value of the byte order flag for each of the two orders.
+     */
+    static final byte XDR = 0, NDR = 1;
+
+    /**
+     * Type codes of the geometries which have one, shared by the encoder and 
the parser.
+     * They are held apart rather than declared on {@code WellKnownBinary} 
because a field
+     * named after a geometry type would obscure the type of the same name.
+     */
+    static final class Codes {
+        /**
+         * The codes of <cite>OGC Simple Feature Access 1.2.1</cite> and 
<cite>ISO 13249-3</cite>.
+         * Codes 13 and 14, which stand for the abstract {@code Curve} and 
{@code Surface} types,
+         * are deliberately absent: no geometry can be of an abstract type.
+         */
+        static final int 
+                POINT = 1, 
+                LINESTRING = 2, 
+                POLYGON = 3, 
+                MULTIPOINT = 4, 
+                MULTILINESTRING = 5,
+                MULTIPOLYGON = 6, 
+                GEOMETRYCOLLECTION = 7, 
+                CIRCULARSTRING = 8, 
+                COMPOUNDCURVE = 9,
+                CURVEPOLYGON = 10, 
+                MULTICURVE = 11, 
+                MULTISURFACE = 12, 
+                POLYHEDRALSURFACE = 15,
+                TIN = 16, 
+                TRIANGLE = 17;
+
+        /**
+         * Value added to a type code for the {@code Z}, {@code M} and {@code 
ZM} flags.
+         * The {@code ZM} flag is the sum of the two others.
+         */
+        static final int Z_OFFSET = 1000, M_OFFSET = 2000;
+
+        /**
+         * Do not allow instantiation of this holder of constants.
+         */
+        private Codes() {
+        }
+    }
+
+    /**
+     * Byte order of the written geometries. Never null. Both orders are read 
whatever this is.
+     */
+    private final ByteOrder byteOrder;
+
+    /**
+     * Creates a codec writing geometries in big endian order, also known as 
XDR, which is the
+     * order of the byte sequences of the standard. Both orders are read.
+     */
+    public WellKnownBinary() {
+        byteOrder = ByteOrder.BIG_ENDIAN;
+    }
+
+    /**
+     * Creates a codec writing geometries in the given byte order. Both orders 
are read whatever
+     * the order given here.
+     *
+     * @param  byteOrder  order of the multi-byte values to write, not null.
+     */
+    public WellKnownBinary(final ByteOrder byteOrder) {
+        ArgumentChecks.ensureNonNull("byteOrder", byteOrder);
+        this.byteOrder = byteOrder;
+    }
+
+    /**
+     * Returns the Well-Known Binary of the given geometry.
+     *
+     * @param  geom  the geometry to encode, not null.
+     * @return the geometry in Well-Known Binary.
+     * @throws IllegalArgumentException if the geometry, or one of the 
geometries it contains,
+     *         has no Well-Known Binary representation, or if its positions 
are neither 2
+     *         nor 3 dimensional.
+     */
+    public byte[] encode(final Geometry geom) {
+        ArgumentChecks.ensureNonNull("geom", geom);
+        final Output out = new Output(byteOrder);
+        format(out, geom);
+        return out.toArray();
+    }
+
+    /**
+     * Returns the geometry described by the given Well-Known Binary.
+     *
+     * @param  geom  the Well-Known Binary to decode, not null.
+     * @return the decoded geometry.
+     * @throws IllegalArgumentException if the bytes are malformed, or name a 
geometry type which
+     *         is not in the table of this class javadoc.
+     */
+    public Geometry decode(final byte[] geom) {
+        return decode(geom, null);
+    }
+
+    /**
+     * Returns the geometry described by the given Well-Known Binary, in the 
given coordinate
+     * reference system. Well-Known Binary carries no system of its own.
+     *
+     * @param  geom  the Well-Known Binary to decode, not null.
+     * @param  crs   the coordinate reference system of the coordinates in the 
bytes, or
+     *               {@code null}.
+     * @return the decoded geometry.
+     * @throws IllegalArgumentException if the bytes are malformed, name a 
geometry type which is
+     *         not in the table of this class javadoc, or have a number of 
ordinates which
+     *         contradicts the dimension of {@code crs}.
+     */
+    public Geometry decode(final byte[] geom, final CoordinateReferenceSystem 
crs) {
+        ArgumentChecks.ensureNonNull("geom", geom);
+        return new WellKnownBinaryParser(geom, crs).parse();
+    }
+
+    // ////////////////////////////////////////////////////////////////////////
+    // Encoding ///////////////////////////////////////////////////////////////
+    // ////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Writes the Well-Known Binary of the given geometry, header included.
+     *
+     * <p>The order of the tests below is significant: the geometry interfaces 
form a hierarchy,
+     * so every type must be tested before its supertypes. It mirrors the 
dispatch of
+     * {@code WellKnownText.format(…)}.</p>
+     */
+    private void format(final Output out, final Geometry geometry) {
+        if (geometry instanceof Orientable o && o.getOrientationSign() == 
Orientable.Sign.NEGATIVE) {
+            format(out, o.getReverse());                        // Orientation 
is not representable.
+        } else if (geometry instanceof Empty g) {
+            writeHeader(out, Codes.GEOMETRYCOLLECTION, g);
+            out.writeInt(0);
+        } else if (geometry instanceof Point g) {
+            formatPoint(out, g);
+        } else if (geometry instanceof CircularString g) {      // Before 
Curve.
+            formatPointList(out, Codes.CIRCULARSTRING, g, g.getDataPoints());
+        } else if (geometry instanceof CompoundCurve g) {       // Before 
Curve.
+            formatCompoundCurve(out, g);
+        } else if (geometry instanceof LineString g) {          // Also 
matches LinearRing.
+            formatPointList(out, Codes.LINESTRING, g, g.getDataPoints());
+        } else if (geometry instanceof Triangle g) {            // Before 
Polygon.
+            formatPolygon(out, Codes.TRIANGLE, g);
+        } else if (geometry instanceof Polygon g) {             // Before 
Surface.
+            formatPolygon(out, Codes.POLYGON, g);
+        } else if (geometry instanceof CurvePolygon g) {        // Before 
Surface.
+            formatCurvePolygon(out, g);
+        } else if (geometry instanceof TIN g) {                 // Before 
PolyhedralSurface.
+            formatPatches(out, Codes.TIN, g.getNumPatches(), g::getPatchN, g);
+        } else if (geometry instanceof PolyhedralSurface<?> g) {
+            formatPatches(out, Codes.POLYHEDRALSURFACE, g.getNumPatches(), 
g::getPatchN, g);
+        } else if (geometry instanceof MultiPoint<?> g) {       // Before 
GeometryCollection.
+            formatMembers(out, Codes.MULTIPOINT, g);
+        } else if (geometry instanceof MultiLineString g) {     // Before 
MultiCurve.
+            formatMembers(out, Codes.MULTILINESTRING, g);
+        } else if (geometry instanceof MultiPolygon g) {        // Before 
MultiSurface.
+            formatMembers(out, Codes.MULTIPOLYGON, g);
+        } else if (geometry instanceof MultiCurve<?> g) {       // Before 
GeometryCollection.
+            formatMembers(out, Codes.MULTICURVE, g);
+        } else if (geometry instanceof MultiSurface<?> g) {     // Before 
GeometryCollection.
+            formatMembers(out, Codes.MULTISURFACE, g);
+        } else if (geometry instanceof GeometryCollection<?> g) {
+            formatMembers(out, Codes.GEOMETRYCOLLECTION, g);
+        } else {
+            throw unsupported(geometry);
+        }
+    }
+
+    /**
+     * Writes a point as a single coordinate tuple. An empty point has no form 
of its own in this
+     * format and is written as a tuple of {@link Double#NaN} ordinates, the 
usual convention.
+     */
+    private void formatPoint(final Output out, final Point geometry) {
+        final boolean hasM = writeHeader(out, Codes.POINT, geometry);
+        if (geometry.isEmpty()) {
+            final int dimension = 
geometry.getCoordinateReferenceSystem().getCoordinateSystem().getDimension();
+            for (int i = hasM ? dimension + 1 : dimension; --i >= 0;) {
+                out.writeDouble(Double.NaN);
+            }
+        } else {
+            writePosition(out, geometry.getPosition(), hasM ? 
geometry.getAttribute(AttributesType.ATT_M) : null);
+        }
+    }
+
+    /**
+     * Writes a type whose body is a single count of coordinate tuples 
followed by those tuples.
+     */
+    private void formatPointList(final Output out, final int code,
+                                 final Geometry geometry, final DataPoints 
points)
+    {
+        final boolean hasM = writeHeader(out, code, geometry);
+        writePointList(out, points, hasM);
+    }
+
+    /**
+     * Writes a compound curve as a count of components followed by those 
components, each of them
+     * a complete geometry. Unlike the text form, a {@code LineString} 
component keeps its header.
+     */
+    private void formatCompoundCurve(final Output out, final CompoundCurve 
geometry) {
+        writeHeader(out, Codes.COMPOUNDCURVE, geometry);
+        final int n = geometry.getNumCurves();
+        out.writeInt(n);
+        for (int i = 0; i < n; i++) {
+            format(out, geometry.getCurveN(i));
+        }
+    }
+
+    /**
+     * Writes a polygon, or a triangle, as a count of rings followed by those 
rings.
+     * A triangle has no interior ring, so its count is 1 unless it is empty.
+     */
+    private void formatPolygon(final Output out, final int code, final Polygon 
geometry) {
+        final boolean hasM = writeHeader(out, code, geometry);
+        writeRings(out, geometry, hasM);
+    }
+
+    /**
+     * Writes a curve polygon as a count of rings followed by those rings, 
each of them a complete
+     * geometry. This is what lets the rings be of any curve type.
+     */
+    private void formatCurvePolygon(final Output out, final CurvePolygon 
geometry) {
+        writeHeader(out, Codes.CURVEPOLYGON, geometry);
+        if (geometry.isEmpty()) {
+            out.writeInt(0);
+            return;
+        }
+        final int n = geometry.getNumInteriorRing();
+        out.writeInt(n + 1);
+        format(out, geometry.getExteriorRing());
+        for (int i = 0; i < n; i++) {
+            format(out, geometry.getInteriorRingN(i));
+        }
+    }
+
+    /**
+     * Writes a polyhedral surface, or a TIN, as a count of patches followed 
by those patches,
+     * each of them a complete {@code POLYGON} or {@code TRIANGLE} geometry.
+     */
+    private void formatPatches(final Output out, final int code, final int 
count,
+                               final IntFunction<? extends Polygon> patches,
+                               final Geometry geometry)
+    {
+        writeHeader(out, code, geometry);
+        out.writeInt(count);
+        for (int i = 0; i < count; i++) {
+            format(out, patches.apply(i));
+        }
+    }
+
+    /**
+     * Writes a collection as a count of members followed by those members, 
each of them a
+     * complete geometry. Every collection type shares this body, the type 
code alone saying
+     * what the members are allowed to be.
+     */
+    private void formatMembers(final Output out, final int code, final 
GeometryCollection<?> geometry) {
+        writeHeader(out, code, geometry);
+        final int n = geometry.getNumGeometries();
+        out.writeInt(n);
+        for (int i = 0; i < n; i++) {
+            format(out, geometry.getGeometryN(i));
+        }
+    }
+
+    /**
+     * Writes the rings of a polygon: a count followed by one coordinate list 
per ring.
+     * A triangle is written the same way: its exterior ring already closes on 
its first corner.
+     */
+    private void writeRings(final Output out, final Polygon polygon, final 
boolean hasM) {
+        if (polygon.isEmpty()) {
+            out.writeInt(0);
+            return;
+        }
+        final int n = polygon.getNumInteriorRing();
+        out.writeInt(n + 1);
+        writePointList(out, polygon.getExteriorRing().getDataPoints(), hasM);
+        for (int i = 0; i < n; i++) {
+            writePointList(out, polygon.getInteriorRingN(i).getDataPoints(), 
hasM);
+        }
+    }
+
+    /**
+     * Writes a count of coordinate tuples followed by those tuples.
+     */
+    private void writePointList(final Output out, final DataPoints points, 
final boolean hasM) {
+        final int n = points.size();
+        out.writeInt(n);
+        for (int i = 0; i < n; i++) {
+            writePosition(out, points.getPosition(i), hasM ? 
points.getAttribute(i, AttributesType.ATT_M) : null);
+        }
+    }
+
+    /**
+     * Writes the ordinates of one position, followed by its measure if any.
+     */
+    private void writePosition(final Output out, final Tuple<?> position, 
final Tuple<?> measure) {
+        for (int i = 0, n = position.getDimension(); i < n; i++) {
+            out.writeDouble(position.get(i));
+        }
+        if (measure != null) {
+            out.writeDouble(measure.get(0));
+        }
+    }
+
+    /**
+     * Writes the byte order flag and the type code of a geometry, the 
dimension flags included.
+     *
+     * @return whether the positions carry a measure, which the caller has to 
write as the
+     *         ordinate following the position ones.
+     */
+    private boolean writeHeader(final Output out, final int code, final 
Geometry geometry) {
+        final CoordinateReferenceSystem crs = 
geometry.getCoordinateReferenceSystem();
+        if (crs == null) {
+            throw new IllegalArgumentException("Cannot write a " + 
geometry.getGeometryType()
+                    + " in Well-Known Binary:"
+                    + " it has no coordinate reference system, therefore no 
known number of dimensions."
+                    + " An empty collection takes one from the factory method 
which creates it.");
+        }
+        final int dimension = crs.getCoordinateSystem().getDimension();
+        final boolean hasM = hasMeasure(geometry);
+        final int flags;
+        switch (dimension) {
+            case 2:  flags = hasM ? Codes.M_OFFSET : 0; break;
+            case 3:  flags = hasM ? Codes.Z_OFFSET + Codes.M_OFFSET : 
Codes.Z_OFFSET; break;
+            default: throw new IllegalArgumentException("Cannot write a " + 
geometry.getGeometryType()
+                        + " in Well-Known Binary: its positions have " + 
dimension + " dimensions,"
+                        + " but the format defines only 2 and 3.");
+        }
+        out.writeByteOrder();
+        out.writeInt(code + flags);
+        return hasM;
+    }
+
+    /**
+     * Returns whether the given geometry carries the {@linkplain 
AttributesType#ATT_M measure}
+     * attribute, which is what the {@code M} and {@code ZM} flags stand for.
+     */
+    private static boolean hasMeasure(final Geometry geometry) {
+        final AttributesType type = geometry.getAttributesType();
+        return (type != null) && 
type.getAttributeNames().contains(AttributesType.ATT_M);
+    }
+
+    /**
+     * Returns the exception to throw for a geometry which the format cannot 
represent.
+     */
+    private static IllegalArgumentException unsupported(final Geometry 
geometry) {
+        return new IllegalArgumentException("Well-Known Binary defines no 
representation for "
+                + geometry.getClass().getSimpleName() + '.');
+    }
+
+    /**
+     * Growable sequence of bytes, writing the multi-byte values in the order 
given to the
+     * constructor. A {@code ByteBuffer} is not used because the length of the 
result is not
+     * known before the geometry has been walked.
+     */
+    private static final class Output {
+        /**
+         * The bytes written so far. Only the first {@link #length} elements 
are meaningful.
+         */
+        private byte[] array = new byte[64];
+
+        /**
+         * Number of meaningful bytes in {@link #array}.
+         */
+        private int length;
+
+        /**
+         * Whether the multi-byte values are written most significant byte 
first.
+         */
+        private final boolean bigEndian;
+
+        /**
+         * Creates an initially empty output writing in the given byte order.
+         */
+        Output(final ByteOrder byteOrder) {
+            bigEndian = (byteOrder == ByteOrder.BIG_ENDIAN);
+        }
+
+        /**
+         * Writes the flag which tells in which order the values after it are 
written.
+         */
+        void writeByteOrder() {
+            ensure(1);
+            array[length++] = bigEndian ? XDR : NDR;
+        }
+
+        /**
+         * Writes a 32 bits integer, which the format uses for the type codes 
and the counts.
+         */
+        void writeInt(final int value) {
+            ensure(Integer.BYTES);
+            for (int i = 0; i < Integer.BYTES; i++) {
+                final int shift = 8 * (bigEndian ? Integer.BYTES - 1 - i : i);
+                array[length++] = (byte) (value >>> shift);
+            }
+        }
+
+        /**
+         * Writes an IEEE 754 double precision number, which the format uses 
for the ordinates.
+         */
+        void writeDouble(final double value) {
+            final long bits = Double.doubleToLongBits(value);
+            ensure(Double.BYTES);
+            for (int i = 0; i < Double.BYTES; i++) {
+                final int shift = 8 * (bigEndian ? Double.BYTES - 1 - i : i);
+                array[length++] = (byte) (bits >>> shift);
+            }
+        }
+
+        /**
+         * Makes room for the given number of additional bytes.
+         */
+        private void ensure(final int count) {
+            if (length + count > array.length) {
+                array = Arrays.copyOf(array, Math.max(length + count, 
array.length * 2));
+            }
+        }
 
+        /**
+         * Returns the bytes written so far, in a array of exactly the right 
length.
+         */
+        byte[] toArray() {
+            return Arrays.copyOf(array, length);
+        }
+    }
 }
diff --git 
a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
new file mode 100644
index 0000000000..194ac06624
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/adapter/WellKnownBinaryParser.java
@@ -0,0 +1,663 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.geometries.adapter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.Curve;
+import org.apache.sis.geometries.DataPoints;
+import org.apache.sis.geometries.Empty;
+import org.apache.sis.geometries.Geometries;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.GeometryFactory;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.Surface;
+import org.apache.sis.geometries.curve.CompoundCurve;
+import org.apache.sis.geometries.curve.LinearRing;
+import org.apache.sis.geometries.curve.LineString;
+import org.apache.sis.geometries.curve.MultiCurve;
+import org.apache.sis.geometries.curve.MultiLineString;
+import org.apache.sis.geometries.point.MultiPoint;
+import org.apache.sis.geometries.surface.CurvePolygon;
+import org.apache.sis.geometries.surface.MultiPolygon;
+import org.apache.sis.geometries.surface.MultiSurface;
+import org.apache.sis.geometries.surface.Polygon;
+import org.apache.sis.geometries.surface.PolyhedralSurface;
+import org.apache.sis.geometries.surface.TIN;
+import org.apache.sis.geometries.surface.Triangle;
+import org.apache.sis.maths.Array;
+import org.apache.sis.maths.DataType;
+import org.apache.sis.maths.NDArrays;
+import org.apache.sis.maths.SampleSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+
+/**
+ * Recursive descent parser of the Well-Known Binary representation of 
geometries.
+ * One instance parses one byte sequence and is then discarded.
+ *
+ * <p>The structure of the bodies, the set of accepted type codes and the 
handling of the
+ * {@code Z}, {@code M} and {@code ZM} flags are documented on {@link 
WellKnownBinary}, which is
+ * the public face of this class. Everything below is that structure written 
as one method per
+ * type.</p>
+ *
+ * <h2>Dimensions</h2>
+ * A Well-Known Binary is homogeneous: a single geometry cannot mix 2- and 
3-dimensional positions,
+ * because an Apache SIS {@link DataPoints} reports one dimension only. Unlike 
the text form, every
+ * element states its flags, so nothing has to be inferred; but the flags of 
the outermost element
+ * still fix them for the whole sequence, and a nested element contradicting 
them is an error.
+ *
+ * <h2>Byte order</h2>
+ * The order is not parser state: each element carries its own flag and the 
standard allows them to
+ * differ, so {@link #bigEndian} is re-read at the start of every element and 
applies until the next
+ * one is read.
+ *
+ * @author  Johann Sorel (Geomatys)
+ */
+final class WellKnownBinaryParser {
+    /**
+     * Bits of the dimension flags, which are the thousands of a type code.
+     */
+    private static final int FLAG_Z = 1, FLAG_M = 2;
+
+    /**
+     * Sample system of the {@linkplain AttributesType#ATT_M measure} 
attribute, which is a single
+     * value with no coordinate reference system of its own.
+     */
+    private static final SampleSystem MEASURE_SYSTEM = SampleSystem.ofSize(1);
+
+    /**
+     * The bytes being parsed.
+     */
+    private final byte[] data;
+
+    /**
+     * The coordinate reference system given by the caller, or {@code null} 
for deriving an
+     * {@linkplain Geometries#getUndefinedCRS(int) undefined} one from the 
number of ordinates.
+     */
+    private final CoordinateReferenceSystem userCRS;
+
+    /**
+     * Index in {@link #data} of the next byte to read.
+     */
+    private int pos;
+
+    /**
+     * Whether the multi-byte values of the element being read are most 
significant byte first.
+     */
+    private boolean bigEndian;
+
+    /**
+     * Whether {@link #hasZ} and {@link #hasM} have been established by the 
flags of an element.
+     */
+    private boolean flagsKnown;
+
+    /**
+     * Whether positions have a third ordinate, and whether they carry a 
measure.
+     */
+    private boolean hasZ, hasM;
+
+    /**
+     * Creates a parser for the given bytes.
+     *
+     * @param  data  the Well-Known Binary to parse.
+     * @param  crs   the coordinate reference system to give to the 
geometries, or {@code null}.
+     */
+    WellKnownBinaryParser(final byte[] data, final CoordinateReferenceSystem 
crs) {
+        this.data = data;
+        this.userCRS = crs;
+    }
+
+    /**
+     * Parses the whole byte sequence as a single geometry.
+     *
+     * @throws IllegalArgumentException if the bytes are malformed or name an 
unsupported type.
+     */
+    Geometry parse() {
+        final Geometry geometry = parseGeometry();
+        if (pos < data.length) {
+            throw error("Unexpected bytes after the end of the geometry");
+        }
+        return geometry;
+    }
+
+    // ////////////////////////////////////////////////////////////////////////
+    // Productions ////////////////////////////////////////////////////////////
+    // ////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Parses one complete geometry: a byte order flag, a type code and a body.
+     */
+    private Geometry parseGeometry() {
+        readByteOrder();
+        final int code = readInt();
+        if (code < 0) {
+            throw error("Type code " + Integer.toUnsignedString(code) + " is 
out of range."
+                    + " The extended Well-Known Binary of some databases, 
which puts the dimension"
+                    + " flags in the high order bits, is not supported");
+        }
+        final int flags = code / 1000;
+        if (flags > 3) {
+            throw error("Type code " + code + " has no dimension flag: the 
thousands must be"
+                    + " 0 for XY, 1 for Z, 2 for M or 3 for ZM");
+        }
+        applyFlags(flags);
+        switch (code % 1000) {
+            case WellKnownBinary.Codes.POINT:               return 
parsePoint();
+            case WellKnownBinary.Codes.LINESTRING:          return 
GeometryFactory.createLineString(readPointList());
+            case WellKnownBinary.Codes.CIRCULARSTRING:      return 
GeometryFactory.createCircularString(readPointList());
+            case WellKnownBinary.Codes.COMPOUNDCURVE:       return 
parseCompoundCurve();
+            case WellKnownBinary.Codes.POLYGON:             return 
readPolygonBody();
+            case WellKnownBinary.Codes.TRIANGLE:            return 
readTriangleBody();
+            case WellKnownBinary.Codes.CURVEPOLYGON:        return 
parseCurvePolygon();
+            case WellKnownBinary.Codes.POLYHEDRALSURFACE:   return 
parsePolyhedralSurface();
+            case WellKnownBinary.Codes.TIN:                 return parseTIN();
+            case WellKnownBinary.Codes.MULTIPOINT:          return 
parseMultiPoint();
+            case WellKnownBinary.Codes.MULTILINESTRING:     return 
parseMultiLineString();
+            case WellKnownBinary.Codes.MULTICURVE:          return 
parseMultiCurve();
+            case WellKnownBinary.Codes.MULTIPOLYGON:        return 
parseMultiPolygon();
+            case WellKnownBinary.Codes.MULTISURFACE:        return 
parseMultiSurface();
+            case WellKnownBinary.Codes.GEOMETRYCOLLECTION:  return 
parseGeometryCollection();
+            default: throw error("Well-Known Binary defines no geometry type 
of code " + code);
+        }
+    }
+
+    /**
+     * Parses the single coordinate tuple of a point.
+     *
+     * <p>A tuple of {@link Double#NaN} ordinates is the usual way of writing 
an empty point, and
+     * does not map to a {@link Point}: the Apache SIS model has no empty 
point, since a point
+     * sequence of length 0 is not a valid position of a point. It maps to 
{@link Empty} instead,
+     * the geometry which stands for the empty point set whatever its type, 
and which is written
+     * back as an empty {@code GEOMETRYCOLLECTION}.</p>
+     */
+    private Geometry parsePoint() {
+        final double[] tuple = readTuple();
+        for (final double ordinate : tuple) {
+            if (!Double.isNaN(ordinate)) {
+                final Coordinates c = new Coordinates();
+                c.add(tuple);
+                return GeometryFactory.createPoint(c.build());
+            }
+        }
+        return GeometryFactory.createEmpty(crs());
+    }
+
+    /**
+     * Parses a count of components followed by that many complete curves.
+     */
+    private CompoundCurve parseCompoundCurve() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createCompoundCurve(crs());
+        }
+        final Curve[] curves = new Curve[n];
+        for (int i = 0; i < n; i++) {
+            curves[i] = readMember(Curve.class, CompoundCurve.TYPE);
+        }
+        return GeometryFactory.createCompoundCurve(curves);
+    }
+
+    /**
+     * Parses a count of rings followed by that many complete curves, the 
first one being the
+     * exterior ring. A count of zero is the empty curve polygon.
+     */
+    private CurvePolygon parseCurvePolygon() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createCurvePolygon(emptyRing(), List.of());
+        }
+        final Curve exterior = readMember(Curve.class, CurvePolygon.TYPE);
+        final List<Curve> interiors = new ArrayList<>(n - 1);
+        for (int i = 1; i < n; i++) {
+            interiors.add(readMember(Curve.class, CurvePolygon.TYPE));
+        }
+        return GeometryFactory.createCurvePolygon(exterior, interiors);
+    }
+
+    /**
+     * Parses a count of patches followed by that many complete polygons.
+     */
+    private PolyhedralSurface<Polygon> parsePolyhedralSurface() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createPolyhedralSurface(crs(), new 
Polygon[0]);
+        }
+        final Polygon[] patches = new Polygon[n];
+        for (int i = 0; i < n; i++) {
+            patches[i] = readMember(Polygon.class, PolyhedralSurface.TYPE);
+        }
+        return GeometryFactory.createPolyhedralSurface(patches);
+    }
+
+    /**
+     * Parses a count of patches followed by that many complete triangles.
+     */
+    private TIN parseTIN() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createTIN(crs(), new Triangle[0]);
+        }
+        final Triangle[] patches = new Triangle[n];
+        for (int i = 0; i < n; i++) {
+            patches[i] = readMember(Triangle.class, TIN.TYPE);
+        }
+        return GeometryFactory.createTIN(patches);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete points.
+     */
+    private MultiPoint<?> parseMultiPoint() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createMultiPoint(crs());
+        }
+        final Point[] members = new Point[n];
+        for (int i = 0; i < n; i++) {
+            // See parsePoint(): an empty point decodes to Empty, which is not 
a Point.
+            members[i] = readMember(Point.class, MultiPoint.TYPE);
+        }
+        return GeometryFactory.createMultiPoint(members);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete line strings.
+     */
+    private MultiLineString parseMultiLineString() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createMultiLineString(crs());
+        }
+        final LineString[] members = new LineString[n];
+        for (int i = 0; i < n; i++) {
+            members[i] = readMember(LineString.class, MultiLineString.TYPE);
+        }
+        return GeometryFactory.createMultiLineString(members);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete polygons.
+     */
+    private MultiPolygon parseMultiPolygon() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.createMultiPolygon(crs());
+        }
+        final Polygon[] members = new Polygon[n];
+        for (int i = 0; i < n; i++) {
+            members[i] = readMember(Polygon.class, MultiPolygon.TYPE);
+        }
+        return GeometryFactory.createMultiPolygon(members);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete curves, which 
unlike those of a
+     * {@code MULTILINESTRING} may be of any curve type.
+     */
+    private MultiCurve<Curve> parseMultiCurve() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.<Curve>createMultiCurve(crs());
+        }
+        final Curve[] members = new Curve[n];
+        for (int i = 0; i < n; i++) {
+            members[i] = readMember(Curve.class, MultiCurve.TYPE);
+        }
+        return GeometryFactory.createMultiCurve(members);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete surfaces, 
which unlike those of a
+     * {@code MULTIPOLYGON} may be of any surface type.
+     */
+    private MultiSurface<Surface> parseMultiSurface() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.<Surface>createMultiSurface(crs());
+        }
+        final Surface[] members = new Surface[n];
+        for (int i = 0; i < n; i++) {
+            members[i] = readMember(Surface.class, MultiSurface.TYPE);
+        }
+        return GeometryFactory.createMultiSurface(members);
+    }
+
+    /**
+     * Parses a count of members followed by that many complete geometries of 
any type.
+     */
+    private GeometryCollection<Geometry> parseGeometryCollection() {
+        final int n = readCount();
+        if (n == 0) {
+            return GeometryFactory.<Geometry>createGeometryCollection(crs());
+        }
+        final Geometry[] members = new Geometry[n];
+        for (int i = 0; i < n; i++) {
+            members[i] = parseGeometry();
+        }
+        return GeometryFactory.createGeometryCollection(members);
+    }
+
+    // ////////////////////////////////////////////////////////////////////////
+    // Shared productions /////////////////////////////////////////////////////
+    // ////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Parses a count of rings followed by that many coordinate lists, without 
header.
+     * A count of zero is the empty polygon.
+     */
+    private Polygon readPolygonBody() {
+        final List<LinearRing> rings = readRings();
+        if (rings.isEmpty()) {
+            return GeometryFactory.createPolygon(emptyRing(), List.of());
+        }
+        return GeometryFactory.createPolygon(rings.get(0), new 
ArrayList<>(rings.subList(1, rings.size())));
+    }
+
+    /**
+     * Parses the single ring of a triangle, in the same form as the rings of 
a polygon.
+     */
+    private Triangle readTriangleBody() {
+        final List<LinearRing> rings = readRings();
+        if (rings.isEmpty()) {
+            return GeometryFactory.createTriangle(emptyRing());
+        }
+        if (rings.size() != 1) {
+            throw error("A " + Triangle.TYPE + " patch has no interior ring, 
but " + (rings.size() - 1) + " were given");
+        }
+        return GeometryFactory.createTriangle(rings.get(0));
+    }
+
+    /**
+     * Parses a count of rings followed by that many coordinate lists.
+     */
+    private List<LinearRing> readRings() {
+        final int n = readCount();
+        final List<LinearRing> rings = new ArrayList<>(n);
+        for (int i = 0; i < n; i++) {
+            rings.add(GeometryFactory.createLinearRing(readPointList()));
+        }
+        return rings;
+    }
+
+    /**
+     * Parses a count of coordinate tuples followed by that many tuples.
+     */
+    private DataPoints readPointList() {
+        final int n = readCount();
+        final Coordinates coordinates = new Coordinates();
+        for (int i = 0; i < n; i++) {
+            coordinates.add(readTuple());
+        }
+        return coordinates.build();
+    }
+
+    /**
+     * Returns an empty ring, for the zero counts of the types which are made 
of rings.
+     */
+    private LinearRing emptyRing() {
+        return GeometryFactory.createLinearRing(new Coordinates().build());
+    }
+
+    /**
+     * Parses one complete geometry and casts it to the type its container 
requires.
+     *
+     * @param  type       the type the container accepts.
+     * @param  container  name of the container type, for the error message.
+     */
+    private <T> T readMember(final Class<T> type, final String container) {
+        final Geometry geometry = parseGeometry();
+        if (type.isInstance(geometry)) {
+            return type.cast(geometry);
+        }
+        throw error("A " + container + " cannot contain a " + 
geometry.getGeometryType());
+    }
+
+    // ////////////////////////////////////////////////////////////////////////
+    // Dimensions /////////////////////////////////////////////////////////////
+    // ////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Returns the number of ordinates of a position, which is 2 unless the 
bytes have established
+     * that positions carry a <var>z</var> ordinate.
+     */
+    private int positionDimension() {
+        return (flagsKnown && hasZ) ? 3 : 2;
+    }
+
+    /**
+     * Returns the coordinate reference system to give to the geometries, 
which is the one the
+     * caller supplied if any, and an {@linkplain 
Geometries#getUndefinedCRS(int) undefined} one
+     * of the right dimension otherwise.
+     *
+     * @throws IllegalArgumentException if the caller supplied a system whose 
dimension
+     *         contradicts the number of ordinates announced by the flags.
+     */
+    private CoordinateReferenceSystem crs() {
+        final int dimension = positionDimension();
+        if (userCRS == null) {
+            return Geometries.getUndefinedCRS(dimension);
+        }
+        final int actual = userCRS.getCoordinateSystem().getDimension();
+        if (actual != dimension) {
+            throw error("The given coordinate reference system has " + actual 
+ " dimensions,"
+                    + " but the bytes have " + dimension + " ordinates per 
position");
+        }
+        return userCRS;
+    }
+
+    /**
+     * Records the dimension flags of an element. The outermost element fixes 
them for the whole
+     * sequence; a nested element may repeat them but not contradict them.
+     */
+    private void applyFlags(final int flags) {
+        final boolean z = (flags & FLAG_Z) != 0;
+        final boolean m = (flags & FLAG_M) != 0;
+        if (flagsKnown) {
+            if (z != hasZ || m != hasM) {
+                throw error("Dimension flag " + flagName(z, m) + " contradicts 
the "
+                        + flagName(hasZ, hasM) + " established by the 
enclosing geometry");
+            }
+        } else {
+            hasZ = z;
+            hasM = m;
+            flagsKnown = true;
+        }
+    }
+
+    /**
+     * Returns {@code "Z"}, {@code "M"}, {@code "ZM"} or {@code "XY"} for an 
error message.
+     */
+    private static String flagName(final boolean z, final boolean m) {
+        if (z) return m ? "ZM" : "Z";
+        return m ? "M" : "XY";
+    }
+
+    /**
+     * Accumulator of the coordinate tuples of one element, and factory of the
+     * {@link DataPoints} the Apache SIS geometry model is built upon.
+     *
+     * <p>Ordinates are accumulated in a single flat array in row-major order, 
which is the layout
+     * {@link NDArrays#of(SampleSystem, double...)} expects. The measure, when 
there is one, is the
+     * last ordinate of each tuple in the bytes but a separate attribute in 
the model, so
+     * {@link #build()} splits the two apart.</p>
+     */
+    private final class Coordinates {
+        /**
+         * The accumulated ordinates. Only the first {@link #count} elements 
are meaningful.
+         */
+        private double[] values = new double[12];
+
+        /**
+         * Number of meaningful ordinates in {@link #values}, always a 
multiple of {@link #width}.
+         */
+        private int count;
+
+        /**
+         * Number of ordinates per tuple, or 0 if no tuple has been added yet.
+         */
+        private int width;
+
+        /**
+         * Appends one coordinate tuple. Every tuple has the width the 
dimension flags announce,
+         * since {@link WellKnownBinaryParser#readTuple()} reads exactly that 
many ordinates.
+         */
+        void add(final double[] tuple) {
+            width = tuple.length;
+            if (count + width > values.length) {
+                values = Arrays.copyOf(values, Math.max(count + width, 
values.length * 2));
+            }
+            System.arraycopy(tuple, 0, values, count, width);
+            count += width;
+        }
+
+        /**
+         * Builds the point sequence of the accumulated tuples, possibly empty.
+         */
+        DataPoints build() {
+            final int posWidth = positionDimension();
+            final SampleSystem posSystem = SampleSystem.of(crs());
+            final int size = (width != 0) ? count / width : 0;
+            if (size == 0) {
+                final Array empty = NDArrays.of(posSystem, DataType.DOUBLE, 0);
+                if (!hasM) {
+                    return GeometryFactory.createSequence(empty);
+                }
+                return createSequence(empty, NDArrays.of(MEASURE_SYSTEM, 
DataType.DOUBLE, 0));
+            }
+            if (!hasM) {
+                return GeometryFactory.createSequence(NDArrays.of(posSystem, 
Arrays.copyOf(values, count)));
+            }
+            /*
+             * The measure is the ordinate following the position ones in each 
tuple of the bytes,
+             * but a one dimensional attribute of its own in the model.
+             */
+            final double[] p = new double[size * posWidth];
+            final double[] m = new double[size];
+            for (int i = 0; i < size; i++) {
+                System.arraycopy(values, i * width, p, i * posWidth, posWidth);
+                m[i] = values[i * width + posWidth];
+            }
+            return createSequence(NDArrays.of(posSystem, p), 
NDArrays.of(MEASURE_SYSTEM, m));
+        }
+
+        /**
+         * Returns a sequence holding both the positions and the measures.
+         */
+        private DataPoints createSequence(final Array positions, final Array 
measures) {
+            final Map<String,Array> attributes = new LinkedHashMap<>(4);
+            attributes.put(AttributesType.ATT_POSITION, positions);
+            attributes.put(AttributesType.ATT_M, measures);
+            return GeometryFactory.createSequence(attributes);
+        }
+    }
+
+    // ////////////////////////////////////////////////////////////////////////
+    // Primitives /////////////////////////////////////////////////////////////
+    // ////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Reads the ordinates of one coordinate tuple. Their number is not 
written anywhere:
+     * it is the one the dimension flags of the element announce.
+     */
+    private double[] readTuple() {
+        final double[] tuple = new double[positionDimension() + (hasM ? 1 : 
0)];
+        for (int i = 0; i < tuple.length; i++) {
+            tuple[i] = readDouble();
+        }
+        return tuple;
+    }
+
+    /**
+     * Reads a count of elements, and verifies that the bytes which remain 
could hold that many.
+     * The check costs nothing and keeps a malformed count from asking for a 
huge allocation:
+     * no element of any type is written in less than one byte.
+     */
+    private int readCount() {
+        final int count = readInt();
+        if (count < 0 || count > data.length - pos) {
+            throw error("A count of " + Integer.toUnsignedString(count) + " 
elements exceeds the "
+                    + (data.length - pos) + " bytes which remain");
+        }
+        return count;
+    }
+
+    /**
+     * Reads the flag which tells in which order the values after it are 
written.
+     */
+    private void readByteOrder() {
+        final byte flag = readByte();
+        switch (flag) {
+            case WellKnownBinary.XDR: bigEndian = true;  break;
+            case WellKnownBinary.NDR: bigEndian = false; break;
+            default: throw error("Byte order flag " + flag + " is neither 0 
for big endian nor 1 for little endian");
+        }
+    }
+
+    /**
+     * Reads one byte.
+     */
+    private byte readByte() {
+        if (pos >= data.length) {
+            throw error("Unexpected end of the geometry");
+        }
+        return data[pos++];
+    }
+
+    /**
+     * Reads a 32 bits integer in the byte order of the element being read.
+     */
+    private int readInt() {
+        if (pos + Integer.BYTES > data.length) {
+            throw error("Unexpected end of the geometry");
+        }
+        int value = 0;
+        for (int i = 0; i < Integer.BYTES; i++) {
+            final int shift = 8 * (bigEndian ? Integer.BYTES - 1 - i : i);
+            value |= (data[pos++] & 0xFF) << shift;
+        }
+        return value;
+    }
+
+    /**
+     * Reads an IEEE 754 double precision number in the byte order of the 
element being read.
+     */
+    private double readDouble() {
+        if (pos + Double.BYTES > data.length) {
+            throw error("Unexpected end of the geometry");
+        }
+        long bits = 0;
+        for (int i = 0; i < Double.BYTES; i++) {
+            final int shift = 8 * (bigEndian ? Double.BYTES - 1 - i : i);
+            bits |= (long) (data[pos++] & 0xFF) << shift;
+        }
+        return Double.longBitsToDouble(bits);
+    }
+
+    /**
+     * Returns the exception to throw for malformed bytes, pointing at the 
current position.
+     */
+    private IllegalArgumentException error(final String message) {
+        return new IllegalArgumentException(message + ", at offset " + pos + " 
of a Well-Known Binary of "
+                + data.length + " bytes");
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
new file mode 100644
index 0000000000..175f4920eb
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/adapter/WellKnownBinaryTest.java
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.geometries.adapter;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import org.apache.sis.geometries.AttributesType;
+import org.apache.sis.geometries.DataPoints;
+import org.apache.sis.geometries.Empty;
+import org.apache.sis.geometries.Geometries;
+import org.apache.sis.geometries.Geometry;
+import org.apache.sis.geometries.GeometryCollection;
+import org.apache.sis.geometries.GeometryFactory;
+import org.apache.sis.geometries.Point;
+import org.apache.sis.geometries.curve.CircularString;
+import org.apache.sis.geometries.curve.CompoundCurve;
+import org.apache.sis.geometries.curve.LineString;
+import org.apache.sis.geometries.curve.MultiCurve;
+import org.apache.sis.geometries.curve.MultiLineString;
+import org.apache.sis.geometries.point.MultiPoint;
+import org.apache.sis.geometries.surface.CurvePolygon;
+import org.apache.sis.geometries.surface.MultiPolygon;
+import org.apache.sis.geometries.surface.MultiSurface;
+import org.apache.sis.geometries.surface.Polygon;
+import org.apache.sis.geometries.surface.PolyhedralSurface;
+import org.apache.sis.geometries.surface.TIN;
+import org.apache.sis.geometries.surface.Triangle;
+import org.apache.sis.maths.NDArrays;
+import org.apache.sis.maths.SampleSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+// Test dependencies
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests {@link WellKnownBinary}.
+ *
+ * <p>Geometries are built from their Well-Known Text rather than from the 
factory, so that a test
+ * case says in one readable line what it encodes. {@link WellKnownTextTest} 
is what verifies that
+ * those texts mean what they look like.</p>
+ *
+ * @author  Johann Sorel (Geomatys)
+ */
+public final class WellKnownBinaryTest {
+    /**
+     * Bits of {@link Double#NaN}, which is how an empty point is written.
+     */
+    private static final String NAN = "7FF8000000000000";
+
+    /**
+     * The codec under test, writing in the big endian order of the standard.
+     */
+    private final WellKnownBinary wkb = new WellKnownBinary();
+
+    /**
+     * The text codec, used for building the geometries to encode and for 
reading back the
+     * geometries decoded.
+     */
+    private final WellKnownText wkt = new WellKnownText();
+
+    /**
+     * Creates a new test case.
+     */
+    public WellKnownBinaryTest() {
+    }
+
+    /**
+     * Verifies that the geometry of the given text encodes to bytes which 
decode back to a
+     * geometry of the expected type and of the same text. The text is the 
canonical form of the
+     * encoder of {@link WellKnownText}, so the round trip is an equality and 
not merely an
+     * equivalence.
+     */
+    private void assertRoundTrip(final Class<? extends Geometry> type, final 
String text) {
+        final Geometry source = wkt.decode(text);
+        final byte[] bytes = wkb.encode(source);
+        final Geometry target = wkb.decode(bytes);
+        assertInstanceOf(type, target, text);
+        assertEquals(text, wkt.encode(target), "Round trip of " + text);
+        assertArrayEquals(bytes, source.asBinary(), "Geometry.asBinary() of " 
+ text);
+    }
+
+    /**
+     * Tests the two dimensional form of every supported geometry type.
+     */
+    @Test
+    public void testRoundTrip2D() {
+        assertRoundTrip(Point.class,              "POINT (1 2)");
+        assertRoundTrip(LineString.class,         "LINESTRING (0 0, 1 1, 2 
0)");
+        assertRoundTrip(CircularString.class,     "CIRCULARSTRING (0 0, 1 1, 2 
0)");
+        assertRoundTrip(CompoundCurve.class,      "COMPOUNDCURVE ((0 0, 1 1), 
CIRCULARSTRING (1 1, 2 2, 3 1))");
+        assertRoundTrip(Polygon.class,            "POLYGON ((0 0, 4 0, 4 4, 0 
4, 0 0), (1 1, 2 1, 2 2, 1 2, 1 1))");
+        assertRoundTrip(Triangle.class,           "TRIANGLE ((0 0, 1 0, 0 1, 0 
0))");
+        assertRoundTrip(CurvePolygon.class,       "CURVEPOLYGON 
(CIRCULARSTRING (0 0, 2 0, 2 2, 0 2, 0 0))");
+        assertRoundTrip(MultiPoint.class,         "MULTIPOINT ((1 2), (3 4))");
+        assertRoundTrip(MultiLineString.class,    "MULTILINESTRING ((0 0, 1 
1), (2 2, 3 3))");
+        assertRoundTrip(MultiPolygon.class,       "MULTIPOLYGON (((0 0, 1 0, 1 
1, 0 0)), ((2 2, 3 2, 3 3, 2 2)))");
+        assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION (POINT 
(1 2), LINESTRING (0 0, 1 1))");
+    }
+
+    /**
+     * Tests the types whose members keep their own header, and the surface 
patch types.
+     * Those are the extensions of ISO 13249-3 over OGC Simple Feature Access.
+     */
+    @Test
+    public void testRoundTripCurvedAndPatches() {
+        assertRoundTrip(MultiCurve.class,
+                "MULTICURVE (LINESTRING (0 0, 1 1), CIRCULARSTRING (1 1, 2 2, 
3 1))");
+        assertRoundTrip(MultiSurface.class,
+                "MULTISURFACE (POLYGON ((0 0, 1 0, 1 1, 0 0)), CURVEPOLYGON 
(CIRCULARSTRING (0 0, 2 0, 2 2, 0 2, 0 0)))");
+        assertRoundTrip(PolyhedralSurface.class,
+                "POLYHEDRALSURFACE Z (((0 0 0, 1 0 0, 1 1 0, 0 0 0)), ((0 0 0, 
1 1 0, 0 1 0, 0 0 0)))");
+        assertRoundTrip(TIN.class,
+                "TIN Z (((0 0 0, 1 0 0, 1 1 0, 0 0 0)), ((0 0 0, 1 1 0, 0 1 0, 
0 0 0)))");
+    }
+
+    /**
+     * Tests the {@code Z}, {@code M} and {@code ZM} flags, which are the 
thousands of a type code.
+     */
+    @Test
+    public void testDimensionFlags() {
+        assertRoundTrip(Point.class,      "POINT Z (1 2 3)");
+        assertRoundTrip(Point.class,      "POINT M (1 2 3)");
+        assertRoundTrip(Point.class,      "POINT ZM (1 2 3 4)");
+        assertRoundTrip(LineString.class, "LINESTRING Z (0 0 0, 1 1 1)");
+        assertRoundTrip(LineString.class, "LINESTRING M (0 0 5, 1 1 6)");
+        assertRoundTrip(LineString.class, "LINESTRING ZM (0 0 0 5, 1 1 1 6)");
+        assertRoundTrip(MultiPoint.class, "MULTIPOINT ZM ((1 2 3 4), (5 6 7 
8))");
+        assertRoundTrip(Polygon.class,    "POLYGON Z ((0 0 0, 1 0 0, 1 1 0, 0 
0 0))");
+        /*
+         * A collection repeats the flag in the type code of each of its 
members,
+         * since each member carries its own header.
+         */
+        assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION Z (POINT 
Z (1 2 3))");
+        /*
+         * The flags are added to the type code: 1000 for Z, 2000 for M, 3000 
for ZM.
+         */
+        
assertEquals("00000003E93FF000000000000040000000000000004008000000000000",
+                     hex(wkb.encode(wkt.decode("POINT Z (1 2 3)"))));
+    }
+
+    /**
+     * Verifies that the measure is read into the dedicated attribute rather 
than into the
+     * position, and that the position keeps the number of dimensions the flag 
announces.
+     */
+    @Test
+    public void testMeasureIsASeparateAttribute() {
+        final byte[] bytes = wkb.encode(wkt.decode("LINESTRING ZM (0 1 2 5, 3 
4 5 6)"));
+        final LineString line = assertInstanceOf(LineString.class, 
wkb.decode(bytes));
+        final DataPoints points = line.getDataPoints();
+        assertEquals(3, points.getDimension(), "Position dimension");
+        assertEquals(2, points.size());
+        assertArrayEquals(new double[] {0, 1, 2}, 
points.getPosition(0).toArrayDouble());
+        assertArrayEquals(new double[] {3, 4, 5}, 
points.getPosition(1).toArrayDouble());
+        
assertTrue(points.getAttributesType().getAttributeNames().contains(AttributesType.ATT_M));
+        assertEquals(5.0, points.getAttribute(0, AttributesType.ATT_M).get(0));
+        assertEquals(6.0, points.getAttribute(1, AttributesType.ATT_M).get(0));
+        /*
+         * Without the flag, a 2-dimensional geometry carries no measure at 
all.
+         */
+        final LineString plain = assertInstanceOf(LineString.class,
+                wkb.decode(wkb.encode(wkt.decode("LINESTRING (0 1, 3 4)"))));
+        
assertFalse(plain.getDataPoints().getAttributesType().getAttributeNames().contains(AttributesType.ATT_M));
+    }
+
+    /**
+     * Tests the empty form, a count of zero, of every supported geometry type.
+     */
+    @Test
+    public void testEmpty() {
+        assertRoundTrip(LineString.class,         "LINESTRING EMPTY");
+        assertRoundTrip(CircularString.class,     "CIRCULARSTRING EMPTY");
+        assertRoundTrip(CompoundCurve.class,      "COMPOUNDCURVE EMPTY");
+        assertRoundTrip(Polygon.class,            "POLYGON EMPTY");
+        assertRoundTrip(Triangle.class,           "TRIANGLE EMPTY");
+        assertRoundTrip(CurvePolygon.class,       "CURVEPOLYGON EMPTY");
+        assertRoundTrip(TIN.class,                "TIN EMPTY");
+        assertRoundTrip(MultiPoint.class,         "MULTIPOINT EMPTY");
+        assertRoundTrip(MultiLineString.class,    "MULTILINESTRING EMPTY");
+        assertRoundTrip(MultiPolygon.class,       "MULTIPOLYGON EMPTY");
+        assertRoundTrip(GeometryCollection.class, "GEOMETRYCOLLECTION EMPTY");
+        /*
+         * An empty geometry is a header and a count of zero, nothing more.
+         */
+        assertEquals("000000000700000000", 
hex(wkb.encode(wkt.decode("GEOMETRYCOLLECTION EMPTY"))));
+        assertEquals("000000000200000000", 
hex(wkb.encode(wkt.decode("LINESTRING EMPTY"))));
+        assertEquals("000000000300000000", hex(wkb.encode(wkt.decode("POLYGON 
EMPTY"))));
+    }
+
+    /**
+     * Verifies that an {@link Empty} geometry, which has no type of its own, 
is written as the
+     * type-less empty form of the format.
+     */
+    @Test
+    public void testEmptyGeometry() {
+        final Geometry empty = 
GeometryFactory.createEmpty(Geometries.getUndefinedCRS(2));
+        assertEquals("000000000700000000", hex(wkb.encode(empty)));
+        assertEquals("000000000700000000", hex(empty.asBinary()));
+    }
+
+    /**
+     * Verifies the one empty form which does not round-trip: the format 
writes a point as a bare
+     * coordinate tuple, so an empty point is a tuple of {@link Double#NaN}, 
and the model has no
+     * empty point to decode it into.
+     */
+    @Test
+    public void testEmptyPoint() {
+        final byte[] empty2D = bytes("00" + "00000001" + NAN + NAN);
+        final byte[] empty3D = bytes("00" + "000003E9" + NAN + NAN + NAN);
+        assertInstanceOf(Empty.class, wkb.decode(empty2D));
+        assertInstanceOf(Empty.class, wkb.decode(empty3D));
+        assertEquals("000000000700000000", 
hex(wkb.encode(wkb.decode(empty2D))));
+        /*
+         * A single NaN ordinate is a legitimate coordinate, not an empty 
point.
+         */
+        assertInstanceOf(Point.class, wkb.decode(bytes("00" + "00000001" + NAN 
+ "4000000000000000")));
+        /*
+         * There is therefore no way to put an empty point in a multi point.
+         */
+        assertMalformed(bytes("00" + "00000004" + "00000001" + "00" + 
"00000001" + NAN + NAN));
+    }
+
+    /**
+     * Tests the byte order flag: both orders are written on demand, both are 
read whatever the
+     * order the codec writes, and the order may change from one nested 
geometry to the next.
+     */
+    @Test
+    public void testByteOrder() {
+        final Geometry point = wkt.decode("POINT (1 2)");
+        final byte[] bigEndian    = new 
WellKnownBinary(ByteOrder.BIG_ENDIAN).encode(point);
+        final byte[] littleEndian = new 
WellKnownBinary(ByteOrder.LITTLE_ENDIAN).encode(point);
+        assertEquals("00000000013FF00000000000004000000000000000", 
hex(bigEndian));
+        assertEquals("0101000000000000000000F03F0000000000000040", 
hex(littleEndian));
+        assertArrayEquals(bigEndian, wkb.encode(point), "Big endian is the 
default");
+        /*
+         * Whichever order the codec writes, it reads both.
+         */
+        final WellKnownBinary reader = new 
WellKnownBinary(ByteOrder.LITTLE_ENDIAN);
+        assertEquals("POINT (1 2)", wkt.encode(reader.decode(bigEndian)));
+        assertEquals("POINT (1 2)", wkt.encode(reader.decode(littleEndian)));
+        /*
+         * A collection written in one order may hold a member written in the 
other.
+         */
+        final byte[] mixed = concat(bytes("00" + "00000007" + "00000001"), 
littleEndian);
+        assertEquals("GEOMETRYCOLLECTION (POINT (1 2))", 
wkt.encode(wkb.decode(mixed)));
+        assertThrows(NullPointerException.class, () -> new 
WellKnownBinary(null));
+    }
+
+    /**
+     * Tests decoding in a coordinate reference system given by the caller.
+     */
+    @Test
+    public void testDecodeWithCRS() {
+        final CoordinateReferenceSystem crs = Geometries.getUndefinedCRS(3);
+        final byte[] bytes = wkb.encode(wkt.decode("POINT Z (1 2 3)"));
+        final Geometry geometry = wkb.decode(bytes, crs);
+        assertSame(crs, geometry.getCoordinateReferenceSystem());
+        /*
+         * The number of ordinates the flags announce and the dimension of the 
system must agree.
+         */
+        final byte[] flat = wkb.encode(wkt.decode("POINT (1 2)"));
+        assertThrows(IllegalArgumentException.class, () -> wkb.decode(flat, 
crs));
+    }
+
+    /**
+     * Verifies that the geometry types which the format does not define are 
rejected
+     * rather than written in an invented structure.
+     */
+    @Test
+    public void testUnsupportedType() {
+        final DataPoints points = GeometryFactory.createSequence(
+                NDArrays.of(SampleSystem.of(Geometries.getUndefinedCRS(2)), 0, 
0, 1, 1));
+        assertThrows(IllegalArgumentException.class, () -> 
wkb.encode(GeometryFactory.createGeodesic(points)));
+        assertThrows(IllegalArgumentException.class, () -> 
wkb.encode(GeometryFactory.createRhumb(points)));
+    }
+
+    /**
+     * Verifies that a geometry whose positions have more than three 
dimensions is rejected,
+     * since the format has no way to tell them from a measure.
+     */
+    @Test
+    public void testTooManyDimensions() {
+        final Point point = 
GeometryFactory.createPoint(Geometries.getUndefinedCRS(4), 1, 2, 3, 4);
+        assertThrows(IllegalArgumentException.class, () -> wkb.encode(point));
+    }
+
+    /**
+     * Tests the rejection of malformed byte sequences built by altering a 
valid one.
+     */
+    @Test
+    public void testTruncatedAndTrailing() {
+        final byte[] point = wkb.encode(wkt.decode("POINT (1 2)"));
+        assertMalformed(new byte[0]);                                   // Not 
even a byte order flag.
+        assertMalformed(Arrays.copyOf(point, 1));                       // No 
type code.
+        assertMalformed(Arrays.copyOf(point, point.length - 1));        // 
Truncated ordinate.
+        assertMalformed(Arrays.copyOf(point, point.length + 1));        // 
Trailing byte.
+        final byte[] badOrder = point.clone();
+        badOrder[0] = 2;                                                // 
Neither 0 nor 1.
+        assertMalformed(badOrder);
+    }
+
+    /**
+     * Tests the rejection of type codes which the format does not define.
+     */
+    @Test
+    public void testMalformedTypeCode() {
+        assertMalformed(bytes("00" + "00000000" + "00000000"));         // 
Code 0.
+        assertMalformed(bytes("00" + "0000000D" + "00000000"));         // 
Abstract Curve type.
+        assertMalformed(bytes("00" + "000000FF" + "00000000"));         // 
Unknown code.
+        assertMalformed(bytes("00" + "00001389" + "00000000"));         // 
Flags of 5000.
+        assertMalformed(bytes("00" + "80000001" + "00000000"));         // 
Extended WKB flags.
+    }
+
+    /**
+     * Tests the rejection of the structural errors which the counts and the 
type codes can carry.
+     */
+    @Test
+    public void testMalformedStructure() {
+        // A count larger than the bytes which remain.
+        assertMalformed(bytes("00" + "00000002" + "7FFFFFFF"));
+        // A geometry collection announcing Z, holding a member which does not.
+        assertMalformed(concat(bytes("00" + "000003EF" + "00000001"),
+                               wkb.encode(wkt.decode("POINT (1 2)"))));
+        // A triangle with an interior ring.
+        assertMalformed(swapTypeCode(wkb.encode(wkt.decode(
+                "POLYGON ((0 0, 4 0, 4 4, 0 0), (1 1, 2 1, 2 2, 1 1))")), 17));
+        // A multi curve whose member is a polygon.
+        assertMalformed(concat(bytes("00" + "0000000B" + "00000001"),
+                               wkb.encode(wkt.decode("POLYGON ((0 0, 1 0, 1 1, 
0 0))"))));
+        // A multi line string whose member is a circular string.
+        assertMalformed(concat(bytes("00" + "00000005" + "00000001"),
+                               wkb.encode(wkt.decode("CIRCULARSTRING (0 0, 1 
1, 2 0)"))));
+    }
+
+    /**
+     * Verifies that the given bytes are rejected as malformed.
+     */
+    private void assertMalformed(final byte[] data) {
+        assertThrows(IllegalArgumentException.class, () -> wkb.decode(data), 
hex(data));
+    }
+
+    /**
+     * Returns a copy of the given geometry with the type code of its 
outermost element replaced
+     * by the given one, the dimension flags left untouched.
+     */
+    private static byte[] swapTypeCode(final byte[] data, final int code) {
+        final byte[] copy = data.clone();
+        final int flags = ((data[1] & 0xFF) << 24 | (data[2] & 0xFF) << 16
+                         | (data[3] & 0xFF) <<  8 | (data[4] & 0xFF)) / 1000 * 
1000;
+        final int value = flags + code;
+        copy[1] = (byte) (value >>> 24);
+        copy[2] = (byte) (value >>> 16);
+        copy[3] = (byte) (value >>>  8);
+        copy[4] = (byte)  value;
+        return copy;
+    }
+
+    /**
+     * Returns the concatenation of the given byte sequences.
+     */
+    private static byte[] concat(final byte[]... parts) {
+        int length = 0;
+        for (final byte[] part : parts) {
+            length += part.length;
+        }
+        final byte[] result = new byte[length];
+        int offset = 0;
+        for (final byte[] part : parts) {
+            System.arraycopy(part, 0, result, offset, part.length);
+            offset += part.length;
+        }
+        return result;
+    }
+
+    /**
+     * Returns the bytes of the given hexadecimal text, which must have an 
even number of digits.
+     */
+    private static byte[] bytes(final String text) {
+        assertEquals(0, text.length() % 2, text);
+        final byte[] data = new byte[text.length() / 2];
+        for (int i = 0; i < data.length; i++) {
+            data[i] = (byte) Integer.parseInt(text.substring(i * 2, i * 2 + 
2), 16);
+        }
+        return data;
+    }
+
+    /**
+     * Returns the given bytes as upper case hexadecimal digits, which is how 
the expected values
+     * of this class are written.
+     */
+    private static String hex(final byte[] data) {
+        final String digits = "0123456789ABCDEF";
+        final StringBuilder sb = new StringBuilder(data.length * 2);
+        for (final byte b : data) {
+            sb.append(digits.charAt((b >>> 4) & 0xF)).append(digits.charAt(b & 
0xF));
+        }
+        return sb.toString();
+    }
+}

Reply via email to