You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2022/03/01 13:22:25 UTC

[sis] branch feat/style created (now 275fcd4)

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

jsorel pushed a change to branch feat/style
in repository https://gitbox.apache.org/repos/asf/sis.git.


      at 275fcd4  Style : add internal implementation of GeoAPI Style

This branch includes the following new commits:

     new 275fcd4  Style : add internal implementation of GeoAPI Style

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[sis] 01/01: Style : add internal implementation of GeoAPI Style

Posted by js...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch feat/style
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 275fcd4fe2326b85ea51c8092f0770e1ce2eef8a
Author: jsorel <jo...@geomatys.com>
AuthorDate: Wed Feb 23 15:10:57 2022 +0100

    Style : add internal implementation of GeoAPI Style
---
 .../org/apache/sis/internal/style/AnchorPoint.java |  98 ++++
 .../sis/internal/style/ChannelSelection.java       | 124 +++++
 .../org/apache/sis/internal/style/ColorMap.java    |  77 +++
 .../sis/internal/style/ColorReplacement.java       |  77 +++
 .../sis/internal/style/ContrastEnhancement.java    |  99 ++++
 .../org/apache/sis/internal/style/Description.java |  97 ++++
 .../apache/sis/internal/style/Displacement.java    |  98 ++++
 .../apache/sis/internal/style/ExternalGraphic.java | 135 +++++
 .../apache/sis/internal/style/ExternalMark.java    | 126 +++++
 .../sis/internal/style/FeatureTypeStyle.java       | 175 +++++++
 .../java/org/apache/sis/internal/style/Fill.java   | 117 +++++
 .../java/org/apache/sis/internal/style/Font.java   | 129 +++++
 .../org/apache/sis/internal/style/Graphic.java     | 178 +++++++
 .../org/apache/sis/internal/style/GraphicFill.java |  60 +++
 .../apache/sis/internal/style/GraphicLegend.java   |  60 +++
 .../apache/sis/internal/style/GraphicStroke.java   |  87 ++++
 .../apache/sis/internal/style/GraphicalSymbol.java |  24 +
 .../java/org/apache/sis/internal/style/Halo.java   | 101 ++++
 .../apache/sis/internal/style/LabelPlacement.java  |  23 +
 .../apache/sis/internal/style/LinePlacement.java   | 163 ++++++
 .../apache/sis/internal/style/LineSymbolizer.java  | 113 +++++
 .../java/org/apache/sis/internal/style/Mark.java   | 130 +++++
 .../apache/sis/internal/style/PointPlacement.java  | 113 +++++
 .../apache/sis/internal/style/PointSymbolizer.java |  97 ++++
 .../sis/internal/style/PolygonSymbolizer.java      | 144 ++++++
 .../sis/internal/style/RasterSymbolizer.java       | 192 +++++++
 .../java/org/apache/sis/internal/style/Rule.java   | 211 ++++++++
 .../sis/internal/style/SelectedChannelType.java    |  92 ++++
 .../apache/sis/internal/style/ShadedRelief.java    |  92 ++++
 .../java/org/apache/sis/internal/style/Stroke.java | 236 +++++++++
 .../java/org/apache/sis/internal/style/Style.java  | 137 +++++
 .../apache/sis/internal/style/StyleFactory.java    | 550 +++++++++++++++++++++
 .../org/apache/sis/internal/style/Symbolizer.java  | 126 +++++
 .../apache/sis/internal/style/TextSymbolizer.java  | 123 +++++
 .../sis/internal/map/MockFeatureTypeStyle.java     | 104 ----
 .../sis/internal/map/MockLineSymbolizer.java       | 113 -----
 .../java/org/apache/sis/internal/map/MockRule.java | 131 -----
 .../org/apache/sis/internal/map/MockStyle.java     |  86 ----
 .../apache/sis/internal/map/SEPortrayerTest.java   | 127 ++---
 .../sis/internal/style/AbstractStyleTests.java     |  28 ++
 .../apache/sis/internal/style/AnchorPointTest.java |  34 ++
 .../sis/internal/style/ChannelSelectionTest.java   |  53 ++
 .../apache/sis/internal/style/ColorMapTest.java    |  32 ++
 .../sis/internal/style/ColorReplacementTest.java   |  31 ++
 .../internal/style/ContrastEnhancementTest.java    |  47 ++
 .../apache/sis/internal/style/DescriptionTest.java |  38 ++
 .../sis/internal/style/DisplacementTest.java       |  34 ++
 .../sis/internal/style/ExternalGraphicTest.java    |  82 +++
 .../sis/internal/style/ExternalMarkTest.java       |  80 +++
 .../sis/internal/style/FeatureTypeStyleTest.java   | 143 ++++++
 .../org/apache/sis/internal/style/FillTest.java    |  63 +++
 .../org/apache/sis/internal/style/FontTest.java    |  74 +++
 .../sis/internal/style/GraphicStrokeTest.java      |  46 ++
 .../org/apache/sis/internal/style/GraphicTest.java | 108 ++++
 .../org/apache/sis/internal/style/HaloTest.java    |  47 ++
 .../sis/internal/style/LinePlacementTest.java      | 106 ++++
 .../sis/internal/style/LineSymbolizerTest.java     |  47 ++
 .../org/apache/sis/internal/style/MarkTest.java    |  78 +++
 .../sis/internal/style/PointPlacementTest.java     |  63 +++
 .../sis/internal/style/PointSymbolizerTest.java    |  32 ++
 .../sis/internal/style/PolygonSymbolizerTest.java  |  79 +++
 .../sis/internal/style/RasterSymbolizerTest.java   | 127 +++++
 .../org/apache/sis/internal/style/RuleTest.java    | 155 ++++++
 .../internal/style/SelectedChannelTypeTest.java    |  47 ++
 .../sis/internal/style/ShadedReliefTest.java       |  46 ++
 .../org/apache/sis/internal/style/StrokeTest.java  | 154 ++++++
 .../org/apache/sis/internal/style/StyleTest.java   |  92 ++++
 .../apache/sis/internal/style/SymbolizerTest.java  |  77 +++
 .../sis/internal/style/TextSymbolizerTest.java     |  95 ++++
 .../apache/sis/test/suite/PortrayalTestSuite.java  |  31 +-
 70 files changed, 6638 insertions(+), 496 deletions(-)

diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
new file mode 100644
index 0000000..7b44517
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/AnchorPoint.java
@@ -0,0 +1,98 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.AnchorPoint}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class AnchorPoint implements org.opengis.style.AnchorPoint {
+
+    private Expression<Feature,? extends Number> x;
+    private Expression<Feature,? extends Number> y;
+
+    public AnchorPoint() {
+        this(StyleFactory.DEFAULT_ANCHOR_POINT_X, StyleFactory.DEFAULT_ANCHOR_POINT_Y);
+    }
+
+    public AnchorPoint(Expression<Feature,? extends Number> x, Expression<Feature,? extends Number> y) {
+        ArgumentChecks.ensureNonNull("x", x);
+        ArgumentChecks.ensureNonNull("y", y);
+        this.x = x;
+        this.y = y;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getAnchorPointX() {
+        return x;
+    }
+
+    public void setAnchorPointX(Expression<Feature,? extends Number> x) {
+        ArgumentChecks.ensureNonNull("x", x);
+        this.x = x;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getAnchorPointY() {
+        return y;
+    }
+
+    public void setAnchorPointY(Expression<Feature,? extends Number> y) {
+        ArgumentChecks.ensureNonNull("y", y);
+        this.y = y;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 53 * hash + Objects.hashCode(this.x);
+        hash = 53 * hash + Objects.hashCode(this.y);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final AnchorPoint other = (AnchorPoint) obj;
+        if (!Objects.equals(this.x, other.x)) {
+            return false;
+        }
+        return Objects.equals(this.y, other.y);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static AnchorPoint castOrCopy(org.opengis.style.AnchorPoint candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof AnchorPoint) {
+            return (AnchorPoint) candidate;
+        }
+        return new AnchorPoint(candidate.getAnchorPointX(), candidate.getAnchorPointY());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
new file mode 100644
index 0000000..410bcf0
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ChannelSelection.java
@@ -0,0 +1,124 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ChannelSelection}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ChannelSelection implements org.opengis.style.ChannelSelection {
+
+    private SelectedChannelType gray;
+    private SelectedChannelType red;
+    private SelectedChannelType green;
+    private SelectedChannelType blue;
+
+    public ChannelSelection() {
+    }
+
+    public ChannelSelection(SelectedChannelType gray) {
+        this.gray = gray;
+    }
+
+    public ChannelSelection(SelectedChannelType red, SelectedChannelType green, SelectedChannelType blue) {
+        this.red = red;
+        this.green = green;
+        this.blue = blue;
+    }
+
+    private ChannelSelection(org.opengis.style.SelectedChannelType gray, org.opengis.style.SelectedChannelType[] rgb) {
+        this.gray = SelectedChannelType.castOrCopy(gray);
+        if (rgb != null) {
+            this.red = SelectedChannelType.castOrCopy(rgb[0]);
+            this.green = SelectedChannelType.castOrCopy(rgb[1]);
+            this.blue = SelectedChannelType.castOrCopy(rgb[2]);
+        }
+    }
+
+    @Override
+    public SelectedChannelType[] getRGBChannels() {
+        return gray != null ? null :
+                red != null ? new SelectedChannelType[]{red, green, blue} : null;
+    }
+
+    public void setRGBChannels(SelectedChannelType[] rgb) {
+        if (rgb == null) {
+            this.red = null;
+            this.green = null;
+            this.blue = null;
+        } else {
+            this.red = rgb[0];
+            this.green = rgb[1];
+            this.blue = rgb[2];
+        }
+    }
+
+    @Override
+    public SelectedChannelType getGrayChannel() {
+        return gray;
+    }
+
+    public void setGray(SelectedChannelType gray) {
+        this.gray = gray;
+    }
+
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 79 * hash + Objects.hashCode(this.gray);
+        hash = 79 * hash + Objects.hashCode(this.red);
+        hash = 79 * hash + Objects.hashCode(this.green);
+        hash = 79 * hash + Objects.hashCode(this.blue);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ChannelSelection other = (ChannelSelection) obj;
+        if (!Objects.equals(this.gray, other.gray)) {
+            return false;
+        }
+        if (!Objects.equals(this.red, other.red)) {
+            return false;
+        }
+        if (!Objects.equals(this.green, other.green)) {
+            return false;
+        }
+        return Objects.equals(this.blue, other.blue);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ChannelSelection castOrCopy(org.opengis.style.ChannelSelection candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ChannelSelection) {
+            return (ChannelSelection) candidate;
+        }
+        return new ChannelSelection(candidate.getGrayChannel(), candidate.getRGBChannels());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
new file mode 100644
index 0000000..04174c1
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorMap.java
@@ -0,0 +1,77 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ColorMap}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ColorMap implements org.opengis.style.ColorMap {
+
+    private Expression<Feature,?> function;
+
+    public ColorMap() {
+    }
+
+    public ColorMap(Expression<Feature, ?> function) {
+        this.function = function;
+    }
+
+    @Override
+    public Expression<Feature,?> getFunction() {
+        return function;
+    }
+
+    public void setFunction(Expression<Feature, ?> function) {
+        this.function = function;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 97 * hash + Objects.hashCode(this.function);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ColorMap other = (ColorMap) obj;
+        return Objects.equals(this.function, other.function);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ColorMap castOrCopy(org.opengis.style.ColorMap candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ColorMap) {
+            return (ColorMap) candidate;
+        }
+        return new ColorMap(candidate.getFunction());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
new file mode 100644
index 0000000..fa82eb9
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ColorReplacement.java
@@ -0,0 +1,77 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ColorReplacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ColorReplacement implements org.opengis.style.ColorReplacement {
+
+    private Expression<Feature,?> recoding;
+
+    public ColorReplacement() {
+    }
+
+    public ColorReplacement(Expression<Feature, ?> recoding) {
+        this.recoding = recoding;
+    }
+
+    @Override
+    public Expression<Feature,?> getRecoding() {
+        return recoding;
+    }
+
+    public void setRecoding(Expression<Feature, ?> recoding) {
+        this.recoding = recoding;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 11 * hash + Objects.hashCode(this.recoding);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ColorReplacement other = (ColorReplacement) obj;
+        return Objects.equals(this.recoding, other.recoding);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ColorReplacement castOrCopy(org.opengis.style.ColorReplacement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ColorReplacement) {
+            return (ColorReplacement) candidate;
+        }
+        return new ColorReplacement(candidate.getRecoding());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
new file mode 100644
index 0000000..40ad18d
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ContrastEnhancement.java
@@ -0,0 +1,99 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.ContrastMethod;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ContrastEnhancement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ContrastEnhancement implements org.opengis.style.ContrastEnhancement {
+
+    private ContrastMethod method;
+    private Expression<Feature, ? extends Number> gammaValue;
+
+    public ContrastEnhancement() {
+        this(ContrastMethod.NONE, StyleFactory.LITERAL_ONE);
+    }
+
+    public ContrastEnhancement(ContrastMethod method, Expression<Feature, ? extends Number> gammaValue) {
+        ArgumentChecks.ensureNonNull("method", method);
+        ArgumentChecks.ensureNonNull("gammaValue", gammaValue);
+        this.method = method;
+        this.gammaValue = gammaValue;
+    }
+
+    @Override
+    public ContrastMethod getMethod() {
+        return method;
+    }
+
+    public void setMethod(ContrastMethod method) {
+        ArgumentChecks.ensureNonNull("method", method);
+        this.method = method;
+    }
+
+    @Override
+    public Expression<Feature, ? extends Number> getGammaValue() {
+        return gammaValue;
+    }
+
+    public void setGammaValue(Expression<Feature,? extends Number> gammaValue) {
+        ArgumentChecks.ensureNonNull("gammaValue", gammaValue);
+        this.gammaValue = gammaValue;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 37 * hash + Objects.hashCode(this.method);
+        hash = 37 * hash + Objects.hashCode(this.gammaValue);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ContrastEnhancement other = (ContrastEnhancement) obj;
+        if (!Objects.equals(this.method, other.method)) {
+            return false;
+        }
+        return Objects.equals(this.gammaValue, other.gammaValue);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ContrastEnhancement castOrCopy(org.opengis.style.ContrastEnhancement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ContrastEnhancement) {
+            return (ContrastEnhancement) candidate;
+        }
+        return new ContrastEnhancement(candidate.getMethod(), candidate.getGammaValue());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
new file mode 100644
index 0000000..333af9d
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Description.java
@@ -0,0 +1,97 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.style.StyleVisitor;
+import org.opengis.util.InternationalString;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Description}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Description implements org.opengis.style.Description {
+
+    private InternationalString title;
+    private InternationalString abstrat;
+
+    public Description() {
+        this(StyleFactory.EMPTY_STRING, StyleFactory.EMPTY_STRING);
+    }
+
+    public Description(InternationalString title, InternationalString abstrat) {
+        ArgumentChecks.ensureNonNull("title", title);
+        ArgumentChecks.ensureNonNull("abstrat", abstrat);
+        this.title = title;
+        this.abstrat = abstrat;
+    }
+
+    @Override
+    public InternationalString getTitle() {
+        return title;
+    }
+
+    public void setTitle(InternationalString title) {
+        ArgumentChecks.ensureNonNull("title", title);
+        this.title = title;
+    }
+
+    @Override
+    public InternationalString getAbstract() {
+        return abstrat;
+    }
+
+    public void setAbstract(InternationalString abstrat) {
+        ArgumentChecks.ensureNonNull("abstrat", abstrat);
+        this.abstrat = abstrat;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 89 * hash + Objects.hashCode(this.title);
+        hash = 89 * hash + Objects.hashCode(this.abstrat);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Description other = (Description) obj;
+        if (!Objects.equals(this.title, other.title)) {
+            return false;
+        }
+        return Objects.equals(this.abstrat, other.abstrat);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Description castOrCopy(org.opengis.style.Description candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Description) {
+            return (Description) candidate;
+        }
+        return new Description(candidate.getTitle(), candidate.getAbstract());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
new file mode 100644
index 0000000..c2fc255
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Displacement.java
@@ -0,0 +1,98 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Displacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Displacement implements org.opengis.style.Displacement {
+
+    private Expression<Feature,? extends Number> displacementX;
+    private Expression<Feature,? extends Number> displacementY;
+
+    public Displacement() {
+        this(StyleFactory.DEFAULT_DISPLACEMENT_X, StyleFactory.DEFAULT_DISPLACEMENT_Y);
+    }
+
+    public Displacement(Expression<Feature, ? extends Number> displacementX, Expression<Feature, ? extends Number> displacementY) {
+        ArgumentChecks.ensureNonNull("displacementX", displacementX);
+        ArgumentChecks.ensureNonNull("displacementY", displacementY);
+        this.displacementX = displacementX;
+        this.displacementY = displacementY;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getDisplacementX() {
+        return displacementX;
+    }
+
+    public void setDisplacementX(Expression<Feature, ? extends Number> displacementX) {
+        ArgumentChecks.ensureNonNull("displacementX", displacementX);
+        this.displacementX = displacementX;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getDisplacementY() {
+        return displacementY;
+    }
+
+    public void setDisplacementY(Expression<Feature, ? extends Number> displacementY) {
+        ArgumentChecks.ensureNonNull("displacementY", displacementY);
+        this.displacementY = displacementY;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 79 * hash + Objects.hashCode(this.displacementX);
+        hash = 79 * hash + Objects.hashCode(this.displacementY);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Displacement other = (Displacement) obj;
+        if (!Objects.equals(this.displacementX, other.displacementX)) {
+            return false;
+        }
+        return Objects.equals(this.displacementY, other.displacementY);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Displacement castOrCopy(org.opengis.style.Displacement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Displacement) {
+            return (Displacement) candidate;
+        }
+        return new Displacement(candidate.getDisplacementX(), candidate.getDisplacementY());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
new file mode 100644
index 0000000..1bc1127
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalGraphic.java
@@ -0,0 +1,135 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import javax.swing.Icon;
+import org.opengis.metadata.citation.OnlineResource;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ExternalGraphic}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ExternalGraphic implements org.opengis.style.ExternalGraphic, GraphicalSymbol {
+
+    private OnlineResource onlineResource;
+    private Icon inlineContent;
+    private String format;
+    private Collection<ColorReplacement> colorReplacements;
+
+    public ExternalGraphic() {
+    }
+
+    public ExternalGraphic(OnlineResource onlineResource, Icon inlineContent, String format, Collection<ColorReplacement> colorReplacements) {
+        this.onlineResource = onlineResource;
+        this.inlineContent = inlineContent;
+        this.format = format;
+        this.colorReplacements = colorReplacements;
+    }
+
+    @Override
+    public OnlineResource getOnlineResource() {
+        return onlineResource;
+    }
+
+    public void setOnlineResource(OnlineResource onlineResource) {
+        this.onlineResource = onlineResource;
+    }
+
+    @Override
+    public Icon getInlineContent() {
+        return inlineContent;
+    }
+
+    public void setInlineContent(Icon inlineContent) {
+        this.inlineContent = inlineContent;
+    }
+
+    @Override
+    public String getFormat() {
+        return format;
+    }
+
+    public void setFormat(String format) {
+        this.format = format;
+    }
+
+    @Override
+    public Collection<org.opengis.style.ColorReplacement> getColorReplacements() {
+        return (Collection) colorReplacements;
+    }
+
+    public void setColorReplacements(Collection<ColorReplacement> colorReplacements) {
+        this.colorReplacements = colorReplacements;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 11 * hash + Objects.hashCode(this.onlineResource);
+        hash = 11 * hash + Objects.hashCode(this.inlineContent);
+        hash = 11 * hash + Objects.hashCode(this.format);
+        hash = 11 * hash + Objects.hashCode(this.colorReplacements);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ExternalGraphic other = (ExternalGraphic) obj;
+        if (!Objects.equals(this.format, other.format)) {
+            return false;
+        }
+        if (!Objects.equals(this.onlineResource, other.onlineResource)) {
+            return false;
+        }
+        if (!Objects.equals(this.inlineContent, other.inlineContent)) {
+            return false;
+        }
+        return Objects.equals(this.colorReplacements, other.colorReplacements);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ExternalGraphic castOrCopy(org.opengis.style.ExternalGraphic candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ExternalGraphic) {
+            return (ExternalGraphic) candidate;
+        }
+
+        final List<ColorReplacement> cs = new ArrayList<>();
+        for (org.opengis.style.ColorReplacement cr : candidate.getColorReplacements()) {
+            cs.add(ColorReplacement.castOrCopy(cr));
+        }
+
+        return new ExternalGraphic(
+                candidate.getOnlineResource(),
+                candidate.getInlineContent(),
+                candidate.getFormat(),
+                cs);
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java
new file mode 100644
index 0000000..a64ecb5
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ExternalMark.java
@@ -0,0 +1,126 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.swing.Icon;
+import org.opengis.metadata.citation.OnlineResource;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ExternalMark}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ExternalMark implements org.opengis.style.ExternalMark {
+
+    private OnlineResource onlineResource;
+    private Icon inlineContent;
+    private String format;
+    private int markIndex;
+
+    public ExternalMark() {
+    }
+
+    public ExternalMark(OnlineResource onlineResource, Icon inlineContent, String format, int markIndex) {
+        this.onlineResource = onlineResource;
+        this.inlineContent = inlineContent;
+        this.format = format;
+        this.markIndex = markIndex;
+    }
+
+    @Override
+    public OnlineResource getOnlineResource() {
+        return onlineResource;
+    }
+
+    public void setOnlineResource(OnlineResource onlineResource) {
+        this.onlineResource = onlineResource;
+    }
+
+    @Override
+    public Icon getInlineContent() {
+        return inlineContent;
+    }
+
+    public void setInlineContent(Icon inlineContent) {
+        this.inlineContent = inlineContent;
+    }
+
+    @Override
+    public String getFormat() {
+        return format;
+    }
+
+    public void setFormat(String format) {
+        this.format = format;
+    }
+
+    @Override
+    public int getMarkIndex() {
+        return markIndex;
+    }
+
+    public void setMarkIndex(int markIndex) {
+        this.markIndex = markIndex;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 59 * hash + Objects.hashCode(this.onlineResource);
+        hash = 59 * hash + Objects.hashCode(this.inlineContent);
+        hash = 59 * hash + Objects.hashCode(this.format);
+        hash = 59 * hash + this.markIndex;
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ExternalMark other = (ExternalMark) obj;
+        if (this.markIndex != other.markIndex) {
+            return false;
+        }
+        if (!Objects.equals(this.format, other.format)) {
+            return false;
+        }
+        if (!Objects.equals(this.onlineResource, other.onlineResource)) {
+            return false;
+        }
+        return Objects.equals(this.inlineContent, other.inlineContent);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ExternalMark castOrCopy(org.opengis.style.ExternalMark candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ExternalMark) {
+            return (ExternalMark) candidate;
+        }
+        return new ExternalMark(
+                candidate.getOnlineResource(),
+                candidate.getInlineContent(),
+                candidate.getFormat(),
+                candidate.getMarkIndex());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
new file mode 100644
index 0000000..9e80425
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/FeatureTypeStyle.java
@@ -0,0 +1,175 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.opengis.filter.ResourceId;
+import org.opengis.metadata.citation.OnlineResource;
+import org.opengis.style.SemanticType;
+import org.opengis.style.StyleVisitor;
+import org.opengis.util.GenericName;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.FeatureTypeStyle}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class FeatureTypeStyle implements org.opengis.style.FeatureTypeStyle {
+
+    private String name;
+    private Description description;
+    private ResourceId featureInstanceIDs;
+    private final Set<GenericName> featureTypeNames = new HashSet<>();
+    private final Set<SemanticType> semanticTypeIdentifiers = new HashSet<>();
+    private final List<Rule> rules = new ArrayList<>();
+    private OnlineResource onlineResource;
+
+    public FeatureTypeStyle() {
+    }
+
+    public FeatureTypeStyle(String name, Description description, ResourceId resourceId, Set<GenericName> featureTypeNames, Set<SemanticType> semanticTypeIdentifiers, List<Rule> rules, OnlineResource OnlineResource) {
+        this.name = name;
+        this.description = description;
+        this.featureInstanceIDs = resourceId;
+        if (featureTypeNames != null) this.featureTypeNames.addAll(featureTypeNames);
+        if (semanticTypeIdentifiers != null) this.semanticTypeIdentifiers.addAll(semanticTypeIdentifiers);
+        if (rules != null) this.rules.addAll(rules);
+        this.onlineResource = OnlineResource;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Description getDescription() {
+        return description;
+    }
+
+    public void setDescription(Description description) {
+        this.description = description;
+    }
+
+    @Override
+    public ResourceId getFeatureInstanceIDs() {
+        return featureInstanceIDs;
+    }
+
+    public void setFeatureInstanceIDs(ResourceId featureInstanceIDs) {
+        this.featureInstanceIDs = featureInstanceIDs;
+    }
+
+    @Override
+    public Set<GenericName> featureTypeNames() {
+        return featureTypeNames;
+    }
+
+    @Override
+    public Set<SemanticType> semanticTypeIdentifiers() {
+        return semanticTypeIdentifiers;
+    }
+
+    @Override
+    public List<Rule> rules() {
+        return rules;
+    }
+
+    @Override
+    public OnlineResource getOnlineResource() {
+        return onlineResource;
+    }
+
+    public void setOnlineResource(OnlineResource OnlineResource) {
+        this.onlineResource = OnlineResource;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 53 * hash + Objects.hashCode(this.name);
+        hash = 53 * hash + Objects.hashCode(this.description);
+        hash = 53 * hash + Objects.hashCode(this.featureInstanceIDs);
+        hash = 53 * hash + Objects.hashCode(this.featureTypeNames);
+        hash = 53 * hash + Objects.hashCode(this.semanticTypeIdentifiers);
+        hash = 53 * hash + Objects.hashCode(this.rules);
+        hash = 53 * hash + Objects.hashCode(this.onlineResource);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final FeatureTypeStyle other = (FeatureTypeStyle) obj;
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.featureInstanceIDs, other.featureInstanceIDs)) {
+            return false;
+        }
+        if (!Objects.equals(this.featureTypeNames, other.featureTypeNames)) {
+            return false;
+        }
+        if (!Objects.equals(this.semanticTypeIdentifiers, other.semanticTypeIdentifiers)) {
+            return false;
+        }
+        if (!Objects.equals(this.rules, other.rules)) {
+            return false;
+        }
+        return Objects.equals(this.onlineResource, other.onlineResource);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static FeatureTypeStyle castOrCopy(org.opengis.style.FeatureTypeStyle candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof FeatureTypeStyle) {
+            return (FeatureTypeStyle) candidate;
+        }
+
+        final List<Rule> rules = new ArrayList<>();
+        for (org.opengis.style.Rule cr : candidate.rules()) {
+            rules.add(Rule.castOrCopy(cr));
+        }
+
+        return new FeatureTypeStyle(
+                candidate.getName(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.getFeatureInstanceIDs(),
+                candidate.featureTypeNames(),
+                candidate.semanticTypeIdentifiers(),
+                rules,
+                candidate.getOnlineResource()
+        );
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
new file mode 100644
index 0000000..1dd56a6
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Fill.java
@@ -0,0 +1,117 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Fill}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Fill implements org.opengis.style.Fill {
+
+    private GraphicFill graphicFill;
+    private Expression<Feature,Color> color;
+    private Expression<Feature, ? extends Number> opacity;
+
+    public Fill() {
+        this(null, StyleFactory.DEFAULT_FILL_COLOR, StyleFactory.DEFAULT_FILL_OPACITY);
+    }
+
+    public Fill(GraphicFill graphicFill, Expression<Feature, Color> color, Expression<Feature, ? extends Number> opacity) {
+        ArgumentChecks.ensureNonNull("color", color);
+        ArgumentChecks.ensureNonNull("opacity", opacity);
+        this.graphicFill = graphicFill;
+        this.color = color;
+        this.opacity = opacity;
+    }
+
+    @Override
+    public GraphicFill getGraphicFill() {
+        return graphicFill;
+    }
+
+    public void setGraphicFill(GraphicFill graphicFill) {
+        this.graphicFill = graphicFill;
+    }
+
+    @Override
+    public Expression<Feature, Color> getColor() {
+        return color;
+    }
+
+    public void setColor(Expression<Feature, Color> color) {
+        ArgumentChecks.ensureNonNull("color", color);
+        this.color = color;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getOpacity() {
+        return opacity;
+    }
+
+    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
+        ArgumentChecks.ensureNonNull("opacity", opacity);
+        this.opacity = opacity;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 23 * hash + Objects.hashCode(this.graphicFill);
+        hash = 23 * hash + Objects.hashCode(this.color);
+        hash = 23 * hash + Objects.hashCode(this.opacity);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Fill other = (Fill) obj;
+        if (!Objects.equals(this.graphicFill, other.graphicFill)) {
+            return false;
+        }
+        if (!Objects.equals(this.color, other.color)) {
+            return false;
+        }
+        return Objects.equals(this.opacity, other.opacity);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Fill castOrCopy(org.opengis.style.Fill candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Fill) {
+            return (Fill) candidate;
+        }
+        return new Fill(
+                GraphicFill.castOrCopy(candidate.getGraphicFill()),
+                candidate.getColor(),
+                candidate.getOpacity());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
new file mode 100644
index 0000000..696eb55
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Font.java
@@ -0,0 +1,129 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Font}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Font implements org.opengis.style.Font {
+
+    private final List<Expression> family = new ArrayList<>();
+    private Expression<Feature,String> style;
+    private Expression<Feature,String> weight;
+    private Expression<Feature,? extends Number> size;
+
+    public Font() {
+        this(Collections.EMPTY_LIST,
+            StyleFactory.DEFAULT_FONT_STYLE,
+            StyleFactory.DEFAULT_FONT_WEIGHT,
+            StyleFactory.DEFAULT_FONT_SIZE);
+    }
+
+    public Font(List<Expression> family, Expression<Feature, String> style, Expression<Feature, String> weight, Expression<Feature, ? extends Number> size) {
+        if (family != null) this.family.addAll(family);
+        this.style = style;
+        this.weight = weight;
+        this.size = size;
+    }
+
+    @Override
+    public List<Expression> getFamily() {
+        return family;
+    }
+
+    @Override
+    public Expression<Feature, String> getStyle() {
+        return style;
+    }
+
+    public void setStyle(Expression<Feature, String> style) {
+        this.style = style;
+    }
+
+    @Override
+    public Expression<Feature, String> getWeight() {
+        return weight;
+    }
+
+    public void setWeight(Expression<Feature, String> weight) {
+        this.weight = weight;
+    }
+
+    @Override
+    public Expression<Feature, ? extends Number> getSize() {
+        return size;
+    }
+
+    public void setSize(Expression<Feature, ? extends Number> size) {
+        this.size = size;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 79 * hash + Objects.hashCode(this.family);
+        hash = 79 * hash + Objects.hashCode(this.style);
+        hash = 79 * hash + Objects.hashCode(this.weight);
+        hash = 79 * hash + Objects.hashCode(this.size);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Font other = (Font) obj;
+        if (!Objects.equals(this.family, other.family)) {
+            return false;
+        }
+        if (!Objects.equals(this.style, other.style)) {
+            return false;
+        }
+        if (!Objects.equals(this.weight, other.weight)) {
+            return false;
+        }
+        return Objects.equals(this.size, other.size);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Font castOrCopy(org.opengis.style.Font candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Font) {
+            return (Font) candidate;
+        }
+        return new Font(
+                candidate.getFamily(),
+                candidate.getStyle(),
+                candidate.getWeight(),
+                candidate.getSize());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
new file mode 100644
index 0000000..53dd85d
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Graphic.java
@@ -0,0 +1,178 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Graphic}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class Graphic implements org.opengis.style.Graphic {
+
+    private final List<GraphicalSymbol> graphicalSymbols = new ArrayList<>();
+    private Expression<Feature,? extends Number> opacity;
+    private Expression<Feature,? extends Number> size;
+    private Expression<Feature,? extends Number> rotation;
+    private AnchorPoint anchorPoint;
+    private Displacement displacement;
+
+    public static Graphic createDefault() {
+        final List<GraphicalSymbol> symbols = new ArrayList<>();
+        symbols.add(new Mark());
+        return new Graphic(symbols,
+                StyleFactory.DEFAULT_GRAPHIC_OPACITY,
+                StyleFactory.DEFAULT_GRAPHIC_SIZE,
+                StyleFactory.DEFAULT_GRAPHIC_ROTATION,
+                new AnchorPoint(),
+                new Displacement());
+    }
+
+    public Graphic() {
+
+    }
+
+    public Graphic(List<GraphicalSymbol> graphicalSymbols,
+            Expression<Feature, ? extends Number> opacity,
+            Expression<Feature, ? extends Number> size,
+            Expression<Feature, ? extends Number> rotation,
+            AnchorPoint anchorPoint,
+            Displacement displacement) {
+        if (graphicalSymbols!= null) this.graphicalSymbols.addAll(graphicalSymbols);
+        this.opacity = opacity;
+        this.size = size;
+        this.rotation = rotation;
+        this.anchorPoint = anchorPoint;
+        this.displacement = displacement;
+    }
+
+    @Override
+    public List<org.opengis.style.GraphicalSymbol> graphicalSymbols() {
+        return (List) graphicalSymbols;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getOpacity() {
+        return opacity;
+    }
+
+    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
+        this.opacity = opacity;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getSize() {
+        return size;
+    }
+
+    public void setSize(Expression<Feature, ? extends Number> size) {
+        this.size = size;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getRotation() {
+        return rotation;
+    }
+
+    public void setRotation(Expression<Feature, ? extends Number> rotation) {
+        this.rotation = rotation;
+    }
+
+    @Override
+    public AnchorPoint getAnchorPoint() {
+        return anchorPoint;
+    }
+
+    public void setAnchorPoint(AnchorPoint anchorPoint) {
+        this.anchorPoint = anchorPoint;
+    }
+
+    @Override
+    public Displacement getDisplacement() {
+        return displacement;
+    }
+
+    public void setDisplacement(Displacement displacement) {
+        this.displacement = displacement;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 17 * hash + Objects.hashCode(this.graphicalSymbols);
+        hash = 17 * hash + Objects.hashCode(this.opacity);
+        hash = 17 * hash + Objects.hashCode(this.size);
+        hash = 17 * hash + Objects.hashCode(this.rotation);
+        hash = 17 * hash + Objects.hashCode(this.anchorPoint);
+        hash = 17 * hash + Objects.hashCode(this.displacement);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Graphic other = (Graphic) obj;
+        if (!Objects.equals(this.graphicalSymbols, other.graphicalSymbols)) {
+            return false;
+        }
+        if (!Objects.equals(this.opacity, other.opacity)) {
+            return false;
+        }
+        if (!Objects.equals(this.size, other.size)) {
+            return false;
+        }
+        if (!Objects.equals(this.rotation, other.rotation)) {
+            return false;
+        }
+        if (!Objects.equals(this.anchorPoint, other.anchorPoint)) {
+            return false;
+        }
+        return Objects.equals(this.displacement, other.displacement);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Graphic castOrCopy(org.opengis.style.Graphic candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Graphic) {
+            return (Graphic) candidate;
+        }
+
+        final List<GraphicalSymbol> cs = new ArrayList<>();
+        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
+            cs.add(GraphicalSymbol.castOrCopy(cr));
+        }
+        return new Graphic(
+                cs,
+                candidate.getOpacity(),
+                candidate.getSize(),
+                candidate.getRotation(),
+                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
+                Displacement.castOrCopy(candidate.getDisplacement()));
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
new file mode 100644
index 0000000..a54789e
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicFill.java
@@ -0,0 +1,60 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.GraphicFill}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class GraphicFill extends Graphic implements org.opengis.style.GraphicFill {
+
+    public GraphicFill() {
+    }
+
+    public GraphicFill(List<GraphicalSymbol> graphicalSymbols,
+            Expression<Feature, Number> opacity,
+            Expression<Feature, Number> size,
+            Expression<Feature, Number> rotation,
+            AnchorPoint anchorPoint,
+            Displacement displacement) {
+        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static GraphicFill castOrCopy(org.opengis.style.GraphicFill candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof GraphicFill) {
+            return (GraphicFill) candidate;
+        }
+        final List<org.apache.sis.internal.style.GraphicalSymbol> cs = new ArrayList<>();
+        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
+            cs.add(org.apache.sis.internal.style.GraphicalSymbol.castOrCopy(cr));
+        }
+        return new GraphicFill(
+                cs,
+                candidate.getOpacity(),
+                candidate.getSize(),
+                candidate.getRotation(),
+                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
+                Displacement.castOrCopy(candidate.getDisplacement()));
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
new file mode 100644
index 0000000..1e9f795
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicLegend.java
@@ -0,0 +1,60 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.GraphicLegend}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class GraphicLegend extends Graphic implements org.opengis.style.GraphicLegend {
+
+    public GraphicLegend() {
+    }
+
+    public GraphicLegend(List<GraphicalSymbol> graphicalSymbols,
+            Expression<Feature, Number> opacity,
+            Expression<Feature, Number> size,
+            Expression<Feature, Number> rotation,
+            AnchorPoint anchorPoint,
+            Displacement displacement) {
+        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static GraphicLegend castOrCopy(org.opengis.style.GraphicLegend candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof GraphicLegend) {
+            return (GraphicLegend) candidate;
+        }
+        final List<GraphicalSymbol> cs = new ArrayList<>();
+        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
+            cs.add(GraphicalSymbol.castOrCopy(cr));
+        }
+        return new GraphicLegend(
+                cs,
+                candidate.getOpacity(),
+                candidate.getSize(),
+                candidate.getRotation(),
+                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
+                Displacement.castOrCopy(candidate.getDisplacement()));
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
new file mode 100644
index 0000000..56b4e70
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicStroke.java
@@ -0,0 +1,87 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.GraphicStroke}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class GraphicStroke extends Graphic implements org.opengis.style.GraphicStroke {
+
+    private Expression<Feature,? extends Number> initialGap;
+    private Expression<Feature,? extends Number> gap;
+
+    public GraphicStroke() {
+    }
+
+    public GraphicStroke(List<GraphicalSymbol> graphicalSymbols,
+            Expression<Feature, ? extends Number> opacity,
+            Expression<Feature, ? extends Number> size,
+            Expression<Feature, ? extends Number> rotation,
+            AnchorPoint anchorPoint,
+            Displacement displacement,
+            Expression<Feature, ? extends Number> initialGap,
+            Expression<Feature, ? extends Number> gap) {
+        super(graphicalSymbols, opacity, size, rotation, anchorPoint, displacement);
+        this.initialGap = initialGap;
+        this.gap = gap;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getInitialGap() {
+        return initialGap;
+    }
+
+    public void setInitialGap(Expression<Feature, ? extends Number> initialGap) {
+        this.initialGap = initialGap;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getGap() {
+        return gap;
+    }
+
+    public void setGap(Expression<Feature, ? extends Number> gap) {
+        this.gap = gap;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static GraphicStroke castOrCopy(org.opengis.style.GraphicStroke candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof GraphicStroke) {
+            return (GraphicStroke) candidate;
+        }
+        final List<GraphicalSymbol> cs = new ArrayList<>();
+        for (org.opengis.style.GraphicalSymbol cr : candidate.graphicalSymbols()) {
+            cs.add(GraphicalSymbol.castOrCopy(cr));
+        }
+        return new GraphicStroke(
+                cs,
+                candidate.getOpacity(),
+                candidate.getSize(),
+                candidate.getRotation(),
+                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
+                Displacement.castOrCopy(candidate.getDisplacement()),
+                candidate.getInitialGap(),
+                candidate.getGap());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
new file mode 100644
index 0000000..0805ba7
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/GraphicalSymbol.java
@@ -0,0 +1,24 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface GraphicalSymbol extends  org.opengis.style.GraphicalSymbol {
+
+    public static GraphicalSymbol castOrCopy(org.opengis.style.GraphicalSymbol candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof org.opengis.style.Mark) {
+            return Mark.castOrCopy((org.opengis.style.Mark) candidate);
+        } else if (candidate instanceof org.opengis.style.ExternalGraphic) {
+            return ExternalGraphic.castOrCopy((org.opengis.style.ExternalGraphic) candidate);
+        } else {
+            throw new IllegalArgumentException("Unexpected symbol type " + candidate.getClass().getName());
+        }
+    }
+
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
new file mode 100644
index 0000000..7db279d
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Halo.java
@@ -0,0 +1,101 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Halo}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Halo implements org.opengis.style.Halo {
+
+    private Fill fill;
+    private Expression<Feature,? extends Number> radius;
+
+    public Halo() {
+        this(new Fill(null, StyleFactory.LITERAL_WHITE, StyleFactory.DEFAULT_FILL_OPACITY),
+                StyleFactory.DEFAULT_HALO_RADIUS);
+    }
+
+    public Halo(Fill fill, Expression<Feature, ? extends Number> radius) {
+        ArgumentChecks.ensureNonNull("fill", fill);
+        ArgumentChecks.ensureNonNull("radius", radius);
+        this.fill = fill;
+        this.radius = radius;
+    }
+
+    @Override
+    public Fill getFill() {
+        return fill;
+    }
+
+    public void setFill(Fill fill) {
+        ArgumentChecks.ensureNonNull("fill", fill);
+        this.fill = fill;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getRadius() {
+        return radius;
+    }
+
+    public void setRadius(Expression<Feature, ? extends Number> radius) {
+        ArgumentChecks.ensureNonNull("radius", radius);
+        this.radius = radius;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 37 * hash + Objects.hashCode(this.fill);
+        hash = 37 * hash + Objects.hashCode(this.radius);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Halo other = (Halo) obj;
+        if (!Objects.equals(this.fill, other.fill)) {
+            return false;
+        }
+        return Objects.equals(this.radius, other.radius);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Halo castOrCopy(org.opengis.style.Halo candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Halo) {
+            return (Halo) candidate;
+        }
+        return new Halo(
+                Fill.castOrCopy(candidate.getFill()),
+                candidate.getRadius());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
new file mode 100644
index 0000000..d07fbb0
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LabelPlacement.java
@@ -0,0 +1,23 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public interface LabelPlacement extends org.opengis.style.LabelPlacement{
+
+    public static LabelPlacement castOrCopy(org.opengis.style.LabelPlacement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof org.opengis.style.PointPlacement) {
+            return PointPlacement.castOrCopy((org.opengis.style.PointPlacement) candidate);
+        } else if (candidate instanceof org.opengis.style.LinePlacement) {
+            return LinePlacement.castOrCopy((org.opengis.style.LinePlacement) candidate);
+        } else {
+            throw new IllegalArgumentException("Unexpected symbol type " + candidate.getClass().getName());
+        }
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
new file mode 100644
index 0000000..27d8f6c
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LinePlacement.java
@@ -0,0 +1,163 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.LinePlacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class LinePlacement implements LabelPlacement, org.opengis.style.LinePlacement {
+
+    private Expression<Feature,? extends Number> perpendicularOffset;
+    private Expression<Feature,? extends Number> initialGap;
+    private Expression<Feature,? extends Number> gap;
+    private boolean repeated;
+    private boolean aligned;
+    private boolean generalizeLine;
+
+    public LinePlacement() {
+    }
+
+    public LinePlacement(
+            Expression<Feature, Number> perpendicularOffset,
+            Expression<Feature, Number> initialGap,
+            Expression<Feature, Number> gap,
+            boolean repeated, boolean aligned, boolean generalizeLine) {
+        this.perpendicularOffset = perpendicularOffset;
+        this.initialGap = initialGap;
+        this.gap = gap;
+        this.repeated = repeated;
+        this.aligned = aligned;
+        this.generalizeLine = generalizeLine;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getPerpendicularOffset() {
+        return perpendicularOffset;
+    }
+
+    public void setPerpendicularOffset(Expression<Feature, ? extends Number> perpendicularOffset) {
+        this.perpendicularOffset = perpendicularOffset;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getInitialGap() {
+        return initialGap;
+    }
+
+    public void setInitialGap(Expression<Feature, ? extends Number> initialGap) {
+        this.initialGap = initialGap;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getGap() {
+        return gap;
+    }
+
+    public void setGap(Expression<Feature, ? extends Number> gap) {
+        this.gap = gap;
+    }
+
+    @Override
+    public boolean isRepeated() {
+        return repeated;
+    }
+
+    public void setRepeated(boolean repeated) {
+        this.repeated = repeated;
+    }
+
+    @Override
+    public boolean IsAligned() {
+        return aligned;
+    }
+
+    public void setAligned(boolean aligned) {
+        this.aligned = aligned;
+    }
+
+    @Override
+    public boolean isGeneralizeLine() {
+        return generalizeLine;
+    }
+
+    public void setGeneralizeLine(boolean generalizeLine) {
+        this.generalizeLine = generalizeLine;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 97 * hash + Objects.hashCode(this.perpendicularOffset);
+        hash = 97 * hash + Objects.hashCode(this.initialGap);
+        hash = 97 * hash + Objects.hashCode(this.gap);
+        hash = 97 * hash + (this.repeated ? 1 : 0);
+        hash = 97 * hash + (this.aligned ? 1 : 0);
+        hash = 97 * hash + (this.generalizeLine ? 1 : 0);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final LinePlacement other = (LinePlacement) obj;
+        if (this.repeated != other.repeated) {
+            return false;
+        }
+        if (this.aligned != other.aligned) {
+            return false;
+        }
+        if (this.generalizeLine != other.generalizeLine) {
+            return false;
+        }
+        if (!Objects.equals(this.perpendicularOffset, other.perpendicularOffset)) {
+            return false;
+        }
+        if (!Objects.equals(this.initialGap, other.initialGap)) {
+            return false;
+        }
+        return Objects.equals(this.gap, other.gap);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static LinePlacement castOrCopy(org.opengis.style.LinePlacement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof LinePlacement) {
+            return (LinePlacement) candidate;
+        }
+        return new LinePlacement(
+                candidate.getPerpendicularOffset(),
+                candidate.getInitialGap(),
+                candidate.getGap(),
+                candidate.isRepeated(),
+                candidate.IsAligned(),
+                candidate.isGeneralizeLine());
+    }
+
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
new file mode 100644
index 0000000..26f9a8e
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/LineSymbolizer.java
@@ -0,0 +1,113 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.LineSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class LineSymbolizer extends Symbolizer implements org.opengis.style.LineSymbolizer {
+
+    private Expression<Feature,? extends Number> perpendicularOffset;
+    private Stroke stroke;
+
+    public static LineSymbolizer createDefault() {
+        return new LineSymbolizer(null, null, new Description(),
+                StyleFactory.DEFAULT_UOM, new Stroke(), StyleFactory.LITERAL_ZERO);
+    }
+
+    public LineSymbolizer() {
+        super();
+    }
+
+    public LineSymbolizer(String name, Expression geometry, Description description,
+            Unit<Length> unit, Stroke stroke, Expression offset) {
+        super(name, geometry, description, unit);
+        this.stroke = stroke;
+        this.perpendicularOffset = offset;
+    }
+
+    @Override
+    public Stroke getStroke() {
+        return stroke;
+    }
+
+    public void setStroke(Stroke stroke) {
+        this.stroke = stroke;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getPerpendicularOffset() {
+        return perpendicularOffset;
+    }
+
+    public void setPerpendicularOffset(Expression<Feature, ? extends Number> perpendicularOffset) {
+        this.perpendicularOffset = perpendicularOffset;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 89 * hash + Objects.hashCode(this.perpendicularOffset);
+        hash = 89 * hash + Objects.hashCode(this.stroke);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final LineSymbolizer other = (LineSymbolizer) obj;
+        if (!Objects.equals(this.perpendicularOffset, other.perpendicularOffset)) {
+            return false;
+        }
+        return Objects.equals(this.stroke, other.stroke);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static LineSymbolizer castOrCopy(org.opengis.style.LineSymbolizer candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof LineSymbolizer) {
+            return (LineSymbolizer) candidate;
+        }
+        return new LineSymbolizer(
+                candidate.getName(),
+                candidate.getGeometry(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.getUnitOfMeasure(),
+                Stroke.castOrCopy(candidate.getStroke()),
+                candidate.getPerpendicularOffset());
+    }
+
+
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
new file mode 100644
index 0000000..730799b
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Mark.java
@@ -0,0 +1,130 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Mark}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Mark implements org.opengis.style.Mark, GraphicalSymbol {
+
+    private Expression<Feature,String> wellKnownName;
+    private ExternalMark externalMark;
+    private Fill fill;
+    private Stroke stroke;
+
+    public Mark() {
+        this(StyleFactory.MARK_SQUARE, null, new Fill(), new Stroke());
+    }
+
+    public Mark(Expression<Feature, String> wellKnownName,
+            ExternalMark externalMark,
+            Fill fill,
+            Stroke stroke) {
+        this.wellKnownName = wellKnownName;
+        this.externalMark = externalMark;
+        this.fill = fill;
+        this.stroke = stroke;
+    }
+
+    @Override
+    public Expression getWellKnownName() {
+        return wellKnownName;
+    }
+
+    public void setWellKnownName(Expression<Feature, String> wellKnownName) {
+        this.wellKnownName = wellKnownName;
+    }
+
+    @Override
+    public ExternalMark getExternalMark() {
+        return externalMark;
+    }
+
+    public void setExternalMark(ExternalMark externalMark) {
+        this.externalMark = externalMark;
+    }
+
+    @Override
+    public Fill getFill() {
+        return fill;
+    }
+
+    public void setFill(Fill fill) {
+        this.fill = fill;
+    }
+
+    @Override
+    public Stroke getStroke() {
+        return stroke;
+    }
+
+    public void setStroke(Stroke stroke) {
+        this.stroke = stroke;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 71 * hash + Objects.hashCode(this.wellKnownName);
+        hash = 71 * hash + Objects.hashCode(this.externalMark);
+        hash = 71 * hash + Objects.hashCode(this.fill);
+        hash = 71 * hash + Objects.hashCode(this.stroke);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Mark other = (Mark) obj;
+        if (!Objects.equals(this.wellKnownName, other.wellKnownName)) {
+            return false;
+        }
+        if (!Objects.equals(this.externalMark, other.externalMark)) {
+            return false;
+        }
+        if (!Objects.equals(this.fill, other.fill)) {
+            return false;
+        }
+        return Objects.equals(this.stroke, other.stroke);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Mark castOrCopy(org.opengis.style.Mark candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Mark) {
+            return (Mark) candidate;
+        }
+        return new Mark(
+                candidate.getWellKnownName(),
+                ExternalMark.castOrCopy(candidate.getExternalMark()),
+                Fill.castOrCopy(candidate.getFill()),
+                Stroke.castOrCopy(candidate.getStroke()));
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
new file mode 100644
index 0000000..ad8e3bf
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointPlacement.java
@@ -0,0 +1,113 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.PointPlacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class PointPlacement implements LabelPlacement, org.opengis.style.PointPlacement {
+
+    private AnchorPoint anchorPoint;
+    private Displacement displacement;
+    private Expression<Feature,? extends Number> rotation;
+
+    public PointPlacement() {
+    }
+
+    public PointPlacement(
+            AnchorPoint anchorPoint,
+            Displacement displacement,
+            Expression<Feature, ? extends Number> rotation) {
+        this.anchorPoint = anchorPoint;
+        this.displacement = displacement;
+        this.rotation = rotation;
+    }
+
+    @Override
+    public AnchorPoint getAnchorPoint() {
+        return anchorPoint;
+    }
+
+    public void setAnchorPoint(AnchorPoint anchorPoint) {
+        this.anchorPoint = anchorPoint;
+    }
+
+    @Override
+    public Displacement getDisplacement() {
+        return displacement;
+    }
+
+    public void setDisplacement(Displacement displacement) {
+        this.displacement = displacement;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getRotation() {
+        return rotation;
+    }
+
+    public void setRotation(Expression<Feature, ? extends Number> rotation) {
+        this.rotation = rotation;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 67 * hash + Objects.hashCode(this.anchorPoint);
+        hash = 67 * hash + Objects.hashCode(this.displacement);
+        hash = 67 * hash + Objects.hashCode(this.rotation);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PointPlacement other = (PointPlacement) obj;
+        if (!Objects.equals(this.anchorPoint, other.anchorPoint)) {
+            return false;
+        }
+        if (!Objects.equals(this.displacement, other.displacement)) {
+            return false;
+        }
+        return Objects.equals(this.rotation, other.rotation);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static PointPlacement castOrCopy(org.opengis.style.PointPlacement candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof PointPlacement) {
+            return (PointPlacement) candidate;
+        }
+        return new PointPlacement(
+                AnchorPoint.castOrCopy(candidate.getAnchorPoint()),
+                Displacement.castOrCopy(candidate.getDisplacement()),
+                candidate.getRotation());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
new file mode 100644
index 0000000..91703c9
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PointSymbolizer.java
@@ -0,0 +1,97 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.PointSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class PointSymbolizer extends Symbolizer implements org.opengis.style.PointSymbolizer {
+
+    private Graphic graphic;
+
+    public static PointSymbolizer createDefault() {
+        return new PointSymbolizer(
+                null,
+                null,
+                new Description(),
+                StyleFactory.DEFAULT_UOM,
+                Graphic.createDefault());
+    }
+
+    public PointSymbolizer() {
+    }
+
+    public PointSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit, Graphic graphic) {
+        super(name, geometry, description, unit);
+        this.graphic = graphic;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public Graphic getGraphic() {
+        return graphic;
+    }
+
+    public void setGraphic(Graphic graphic) {
+        this.graphic = graphic;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 71 * hash + Objects.hashCode(this.graphic);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PointSymbolizer other = (PointSymbolizer) obj;
+        return Objects.equals(this.graphic, other.graphic);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static PointSymbolizer castOrCopy(org.opengis.style.PointSymbolizer candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof PointSymbolizer) {
+            return (PointSymbolizer) candidate;
+        }
+        return new PointSymbolizer(
+                candidate.getName(),
+                candidate.getGeometry(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.getUnitOfMeasure(),
+                Graphic.castOrCopy(candidate.getGraphic())
+            );
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
new file mode 100644
index 0000000..e31b460
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/PolygonSymbolizer.java
@@ -0,0 +1,144 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.PolygonSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class PolygonSymbolizer extends Symbolizer implements org.opengis.style.PolygonSymbolizer {
+
+    private Stroke stroke;
+    private Fill fill;
+    private Displacement displacement;
+    private Expression<Feature,? extends Number> offset;
+
+    public static PolygonSymbolizer createDefault() {
+        return new PolygonSymbolizer(null,null, new Description(),
+                StyleFactory.DEFAULT_UOM, new Stroke(), new Fill(),
+                new Displacement(), StyleFactory.LITERAL_ZERO);
+    }
+
+    public PolygonSymbolizer() {
+    }
+
+    public PolygonSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit, Stroke stroke, Fill fill, Displacement displacement, Expression offset) {
+        super(name, geometry, description, unit);
+        this.stroke = stroke;
+        this.fill = fill;
+        this.displacement = displacement;
+        this.offset = offset;
+    }
+
+    @Override
+    public Stroke getStroke() {
+        return stroke;
+    }
+
+    public void setStroke(Stroke stroke) {
+        this.stroke = stroke;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public Fill getFill() {
+        return fill;
+    }
+
+    public void setFill(Fill fill) {
+        this.fill = fill;
+    }
+
+    @Override
+    public Displacement getDisplacement() {
+        return displacement;
+    }
+
+    public void setDisplacement(Displacement displacement) {
+        this.displacement = displacement;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getPerpendicularOffset() {
+        return offset;
+    }
+
+    public void setPerpendicularOffset(Expression<Feature,? extends Number> offset) {
+        this.offset = offset;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 79 * hash + Objects.hashCode(this.stroke);
+        hash = 79 * hash + Objects.hashCode(this.fill);
+        hash = 79 * hash + Objects.hashCode(this.displacement);
+        hash = 79 * hash + Objects.hashCode(this.offset);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PolygonSymbolizer other = (PolygonSymbolizer) obj;
+        if (!Objects.equals(this.stroke, other.stroke)) {
+            return false;
+        }
+        if (!Objects.equals(this.fill, other.fill)) {
+            return false;
+        }
+        if (!Objects.equals(this.displacement, other.displacement)) {
+            return false;
+        }
+        return Objects.equals(this.offset, other.offset);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static PolygonSymbolizer castOrCopy(org.opengis.style.PolygonSymbolizer candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof PolygonSymbolizer) {
+            return (PolygonSymbolizer) candidate;
+        }
+        return new PolygonSymbolizer(
+                candidate.getName(),
+                candidate.getGeometry(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.getUnitOfMeasure(),
+                Stroke.castOrCopy(candidate.getStroke()),
+                Fill.castOrCopy(candidate.getFill()),
+                Displacement.castOrCopy(candidate.getDisplacement()),
+                candidate.getPerpendicularOffset()
+            );
+    }
+
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
new file mode 100644
index 0000000..0fc5a8a
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/RasterSymbolizer.java
@@ -0,0 +1,192 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.OverlapBehavior;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.RasterSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class RasterSymbolizer extends Symbolizer implements org.opengis.style.RasterSymbolizer {
+
+    private Expression<Feature,? extends Number> opacity;
+    private ChannelSelection channelSelection;
+    private OverlapBehavior overlapBehavior;
+    private ColorMap colorMap;
+    private ContrastEnhancement contrastEnhancement;
+    private ShadedRelief shadedRelief;
+    private org.opengis.style.Symbolizer imageOutline;
+
+    public static RasterSymbolizer createDefault() {
+        return new RasterSymbolizer();
+    }
+
+    public RasterSymbolizer() {
+    }
+
+    public RasterSymbolizer(String name, Expression geometry, Description description, Unit<Length> unit,
+            Expression opacity, ChannelSelection channelSelection, OverlapBehavior overlapsBehaviour,
+            ColorMap colorMap, ContrastEnhancement contrast, ShadedRelief shaded, org.opengis.style.Symbolizer outline) {
+        super(name, geometry, description, unit);
+        this.opacity = opacity;
+        this.channelSelection = channelSelection;
+        this.overlapBehavior = overlapsBehaviour;
+        this.colorMap = colorMap;
+        this.contrastEnhancement = contrast;
+        this.shadedRelief = shaded;
+        this.imageOutline = outline;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getOpacity() {
+        return opacity;
+    }
+
+    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
+        this.opacity = opacity;
+    }
+
+    @Override
+    public ChannelSelection getChannelSelection() {
+        return channelSelection;
+    }
+
+    public void setChannelSelection(ChannelSelection channelSelection) {
+        this.channelSelection = channelSelection;
+    }
+
+    @Override
+    public OverlapBehavior getOverlapBehavior() {
+        return overlapBehavior;
+    }
+
+    public void setOverlapBehavior(OverlapBehavior overlapBehavior) {
+        this.overlapBehavior = overlapBehavior;
+    }
+
+    @Override
+    public ColorMap getColorMap() {
+        return colorMap;
+    }
+
+    public void setColorMap(ColorMap colorMap) {
+        this.colorMap = colorMap;
+    }
+
+    @Override
+    public ContrastEnhancement getContrastEnhancement() {
+        return contrastEnhancement;
+    }
+
+    public void setContrastEnhancement(ContrastEnhancement contrastEnhancement) {
+        this.contrastEnhancement = contrastEnhancement;
+    }
+
+    @Override
+    public ShadedRelief getShadedRelief() {
+        return shadedRelief;
+    }
+
+    public void setShadedRelief(ShadedRelief shadedRelief) {
+        this.shadedRelief = shadedRelief;
+    }
+
+    @Override
+    public org.opengis.style.Symbolizer getImageOutline() {
+        return imageOutline;
+    }
+
+    public void setImageOutline(org.opengis.style.Symbolizer imageOutline) {
+        this.imageOutline = imageOutline;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = super.hashCode();
+        hash = 71 * hash + Objects.hashCode(this.opacity);
+        hash = 71 * hash + Objects.hashCode(this.channelSelection);
+        hash = 71 * hash + Objects.hashCode(this.overlapBehavior);
+        hash = 71 * hash + Objects.hashCode(this.colorMap);
+        hash = 71 * hash + Objects.hashCode(this.contrastEnhancement);
+        hash = 71 * hash + Objects.hashCode(this.shadedRelief);
+        hash = 71 * hash + Objects.hashCode(this.imageOutline);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final RasterSymbolizer other = (RasterSymbolizer) obj;
+        if (!Objects.equals(this.opacity, other.opacity)) {
+            return false;
+        }
+        if (!Objects.equals(this.channelSelection, other.channelSelection)) {
+            return false;
+        }
+        if (this.overlapBehavior != other.overlapBehavior) {
+            return false;
+        }
+        if (!Objects.equals(this.colorMap, other.colorMap)) {
+            return false;
+        }
+        if (!Objects.equals(this.contrastEnhancement, other.contrastEnhancement)) {
+            return false;
+        }
+        if (!Objects.equals(this.shadedRelief, other.shadedRelief)) {
+            return false;
+        }
+        return Objects.equals(this.imageOutline, other.imageOutline);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static RasterSymbolizer castOrCopy(org.opengis.style.RasterSymbolizer candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof RasterSymbolizer) {
+            return (RasterSymbolizer) candidate;
+        }
+        return new RasterSymbolizer(
+                candidate.getName(),
+                candidate.getGeometry(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.getUnitOfMeasure(),
+                candidate.getOpacity(),
+                ChannelSelection.castOrCopy(candidate.getChannelSelection()),
+                candidate.getOverlapBehavior(),
+                ColorMap.castOrCopy(candidate.getColorMap()),
+                ContrastEnhancement.castOrCopy(candidate.getContrastEnhancement()),
+                ShadedRelief.castOrCopy(candidate.getShadedRelief()),
+                candidate.getImageOutline()
+            );
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
new file mode 100644
index 0000000..5adb200
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Rule.java
@@ -0,0 +1,211 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.opengis.filter.Filter;
+import org.opengis.metadata.citation.OnlineResource;
+import org.opengis.style.StyleVisitor;
+import org.opengis.style.Symbolizer;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Rule}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Rule implements org.opengis.style.Rule {
+
+    private String name;
+    private Description description;
+    private GraphicLegend legend;
+    private Filter filter;
+    private boolean elseFilter;
+    private double minScale;
+    private double maxScale = Double.MAX_VALUE;
+    private final List<Symbolizer> symbolizers = new ArrayList<>();
+    private OnlineResource onlineResource;
+
+    public Rule() {
+    }
+
+    public Rule(String name, Description description, GraphicLegend legend, Filter filter, boolean elseFilter, double minScale, double maxScale, List<Symbolizer> symbolizers, OnlineResource onlineResource) {
+        this.name = name;
+        this.description = description;
+        this.legend = legend;
+        this.filter = filter;
+        this.elseFilter = elseFilter;
+        this.minScale = minScale;
+        this.maxScale = maxScale;
+        if (symbolizers != null) this.symbolizers.addAll(symbolizers);
+        this.onlineResource = onlineResource;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Description getDescription() {
+        return description;
+    }
+
+    public void setDescription(Description description) {
+        this.description = description;
+    }
+
+    @Override
+    public GraphicLegend getLegend() {
+        return legend;
+    }
+
+    public void setLegend(GraphicLegend legend) {
+        this.legend = legend;
+    }
+
+    @Override
+    public Filter getFilter() {
+        return filter;
+    }
+
+    public void setFilter(Filter filter) {
+        this.filter = filter;
+    }
+
+    @Override
+    public boolean isElseFilter() {
+        return elseFilter;
+    }
+
+    public void setElseFilter(boolean elseFilter) {
+        this.elseFilter = elseFilter;
+    }
+
+    @Override
+    public double getMinScaleDenominator() {
+        return minScale;
+    }
+
+    public void setMinScaleDenominator(double minScale) {
+        this.minScale = minScale;
+    }
+
+    @Override
+    public double getMaxScaleDenominator() {
+        return maxScale;
+    }
+
+    public void setMaxScaleDenominator(double maxScale) {
+        this.maxScale = maxScale;
+    }
+
+    @Override
+    public List<Symbolizer> symbolizers() {
+        return symbolizers;
+    }
+
+    @Override
+    public OnlineResource getOnlineResource() {
+        return onlineResource;
+    }
+
+    public void setOnlineResource(OnlineResource OnlineResource) {
+        this.onlineResource = OnlineResource;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 97 * hash + Objects.hashCode(this.name);
+        hash = 97 * hash + Objects.hashCode(this.description);
+        hash = 97 * hash + Objects.hashCode(this.legend);
+        hash = 97 * hash + Objects.hashCode(this.filter);
+        hash = 97 * hash + (this.elseFilter ? 1 : 0);
+        hash = 97 * hash + (int) (Double.doubleToLongBits(this.minScale) ^ (Double.doubleToLongBits(this.minScale) >>> 32));
+        hash = 97 * hash + (int) (Double.doubleToLongBits(this.maxScale) ^ (Double.doubleToLongBits(this.maxScale) >>> 32));
+        hash = 97 * hash + Objects.hashCode(this.symbolizers);
+        hash = 97 * hash + Objects.hashCode(this.onlineResource);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Rule other = (Rule) obj;
+        if (this.elseFilter != other.elseFilter) {
+            return false;
+        }
+        if (Double.doubleToLongBits(this.minScale) != Double.doubleToLongBits(other.minScale)) {
+            return false;
+        }
+        if (Double.doubleToLongBits(this.maxScale) != Double.doubleToLongBits(other.maxScale)) {
+            return false;
+        }
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.legend, other.legend)) {
+            return false;
+        }
+        if (!Objects.equals(this.filter, other.filter)) {
+            return false;
+        }
+        if (!Objects.equals(this.symbolizers, other.symbolizers)) {
+            return false;
+        }
+        return Objects.equals(this.onlineResource, other.onlineResource);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Rule castOrCopy(org.opengis.style.Rule candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Rule) {
+            return (Rule) candidate;
+        }
+
+        final List<Symbolizer> symbols = new ArrayList<>();
+        for (org.opengis.style.Symbolizer s : candidate.symbolizers()) {
+            symbols.add(org.apache.sis.internal.style.Symbolizer.tryCastOrCopy(s));
+        }
+
+        return new Rule(
+                candidate.getName(),
+                Description.castOrCopy(candidate.getDescription()),
+                GraphicLegend.castOrCopy(candidate.getLegend()),
+                candidate.getFilter(),
+                candidate.isElseFilter(),
+                candidate.getMinScaleDenominator(),
+                candidate.getMaxScaleDenominator(),
+                symbols,
+                candidate.getOnlineResource());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
new file mode 100644
index 0000000..bd4044e
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/SelectedChannelType.java
@@ -0,0 +1,92 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.SelectedChannelType}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class SelectedChannelType implements org.opengis.style.SelectedChannelType {
+
+    private String channelName;
+    private ContrastEnhancement contrastEnhancement;
+
+    public SelectedChannelType() {
+    }
+
+    public SelectedChannelType(String channelName, ContrastEnhancement contrastEnhancement) {
+        this.channelName = channelName;
+        this.contrastEnhancement = contrastEnhancement;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public String getChannelName() {
+        return channelName;
+    }
+
+    public void setChannelName(String channelName) {
+        this.channelName = channelName;
+    }
+
+    @Override
+    public ContrastEnhancement getContrastEnhancement() {
+        return contrastEnhancement;
+    }
+
+    public void setContrastEnhancement(ContrastEnhancement contrastEnhancement) {
+        this.contrastEnhancement = contrastEnhancement;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 3;
+        hash = 97 * hash + Objects.hashCode(this.channelName);
+        hash = 97 * hash + Objects.hashCode(this.contrastEnhancement);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final SelectedChannelType other = (SelectedChannelType) obj;
+        if (!Objects.equals(this.channelName, other.channelName)) {
+            return false;
+        }
+        return Objects.equals(this.contrastEnhancement, other.contrastEnhancement);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static SelectedChannelType castOrCopy(org.opengis.style.SelectedChannelType candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof SelectedChannelType) {
+            return (SelectedChannelType) candidate;
+        }
+        return new SelectedChannelType(
+                candidate.getChannelName(),
+                ContrastEnhancement.castOrCopy(candidate.getContrastEnhancement()));
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
new file mode 100644
index 0000000..8514b5c
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/ShadedRelief.java
@@ -0,0 +1,92 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.ShadedRelief}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class ShadedRelief implements org.opengis.style.ShadedRelief {
+
+    private boolean brightnessOnly;
+    private Expression<Feature,? extends Number> reliefFactor;
+
+    public ShadedRelief() {
+    }
+
+    public ShadedRelief(boolean brightnessOnly, Expression<Feature, ? extends Number> reliefFactor) {
+        this.brightnessOnly = brightnessOnly;
+        this.reliefFactor = reliefFactor;
+    }
+
+    @Override
+    public boolean isBrightnessOnly() {
+        return brightnessOnly;
+    }
+
+    public void setBrightnessOnly(boolean brightnessOnly) {
+        this.brightnessOnly = brightnessOnly;
+    }
+
+    @Override
+    public Expression<Feature, ? extends Number> getReliefFactor() {
+        return reliefFactor;
+    }
+
+    public void setReliefFactor(Expression<Feature, ? extends Number> reliefFactor) {
+        this.reliefFactor = reliefFactor;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 41 * hash + (this.brightnessOnly ? 1 : 0);
+        hash = 41 * hash + Objects.hashCode(this.reliefFactor);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final ShadedRelief other = (ShadedRelief) obj;
+        if (this.brightnessOnly != other.brightnessOnly) {
+            return false;
+        }
+        return Objects.equals(this.reliefFactor, other.reliefFactor);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static ShadedRelief castOrCopy(org.opengis.style.ShadedRelief candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof ShadedRelief) {
+            return (ShadedRelief) candidate;
+        }
+        return new ShadedRelief(candidate.isBrightnessOnly(), candidate.getReliefFactor());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
new file mode 100644
index 0000000..8e93313
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Stroke.java
@@ -0,0 +1,236 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Stroke}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Stroke implements org.opengis.style.Stroke {
+
+    private GraphicFill graphicFill;
+    private GraphicStroke graphicStroke;
+    private Expression<Feature,Color> color;
+    private Expression<Feature,? extends Number> opacity;
+    private Expression<Feature,? extends Number> width;
+    private Expression<Feature,String> lineJoin;
+    private Expression<Feature,String> lineCap;
+    private float[] dashArray;
+    private Expression<Feature,? extends Number> dashOffset;
+
+    public Stroke() {
+        this(null,null,
+             StyleFactory.DEFAULT_STROKE_COLOR,
+             StyleFactory.DEFAULT_STROKE_OPACITY,
+             StyleFactory.DEFAULT_STROKE_WIDTH,
+             StyleFactory.DEFAULT_STROKE_JOIN,
+             StyleFactory.DEFAULT_STROKE_CAP,
+             null,
+             StyleFactory.DEFAULT_STROKE_OFFSET);
+    }
+
+    public Stroke(GraphicFill graphicFill, GraphicStroke graphicStroke,
+            Expression<Feature, Color> color,
+            Expression<Feature, ? extends Number> opacity,
+            Expression<Feature, ? extends Number> width,
+            Expression<Feature, String> lineJoin,
+            Expression<Feature, String> lineCap,
+            float[] dashArray,
+            Expression<Feature, ? extends Number> dashOffset) {
+        ArgumentChecks.ensureNonNull("color", color);
+        ArgumentChecks.ensureNonNull("opacity", opacity);
+        ArgumentChecks.ensureNonNull("width", width);
+        ArgumentChecks.ensureNonNull("lineJoin", lineJoin);
+        ArgumentChecks.ensureNonNull("lineCap", lineCap);
+        ArgumentChecks.ensureNonNull("dashOffset", dashOffset);
+        this.graphicFill = graphicFill;
+        this.graphicStroke = graphicStroke;
+        this.color = color;
+        this.opacity = opacity;
+        this.width = width;
+        this.lineJoin = lineJoin;
+        this.lineCap = lineCap;
+        this.dashArray = dashArray;
+        this.dashOffset = dashOffset;
+    }
+
+    @Override
+    public GraphicFill getGraphicFill() {
+        return graphicFill;
+    }
+
+    public void setGraphicFill(GraphicFill graphicFill) {
+        this.graphicFill = graphicFill;
+    }
+
+    @Override
+    public GraphicStroke getGraphicStroke() {
+        return graphicStroke;
+    }
+
+    public void setGraphicStroke(GraphicStroke graphicStroke) {
+        this.graphicStroke = graphicStroke;
+    }
+
+    @Override
+    public Expression<Feature,Color> getColor() {
+        return color;
+    }
+
+    public void setColor(Expression<Feature, Color> color) {
+        ArgumentChecks.ensureNonNull("color", color);
+        this.color = color;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getOpacity() {
+        return opacity;
+    }
+
+    public void setOpacity(Expression<Feature, ? extends Number> opacity) {
+        ArgumentChecks.ensureNonNull("opacity", opacity);
+        this.opacity = opacity;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getWidth() {
+        return width;
+    }
+
+    public void setWidth(Expression<Feature, ? extends Number> width) {
+        ArgumentChecks.ensureNonNull("width", width);
+        this.width = width;
+    }
+
+    @Override
+    public Expression<Feature,String> getLineJoin() {
+        return lineJoin;
+    }
+
+    public void setLineJoin(Expression<Feature, String> lineJoin) {
+        ArgumentChecks.ensureNonNull("lineJoin", lineJoin);
+        this.lineJoin = lineJoin;
+    }
+
+    @Override
+    public Expression<Feature,String> getLineCap() {
+        return lineCap;
+    }
+
+    public void setLineCap(Expression<Feature, String> lineCap) {
+        ArgumentChecks.ensureNonNull("lineCap", lineCap);
+        this.lineCap = lineCap;
+    }
+
+    @Override
+    public float[] getDashArray() {
+        return dashArray;
+    }
+
+    public void setDashArray(float[] dashArray) {
+        this.dashArray = dashArray;
+    }
+
+    @Override
+    public Expression<Feature,? extends Number> getDashOffset() {
+        return dashOffset;
+    }
+
+    public void setDashOffset(Expression<Feature, ? extends Number> dashOffset) {
+        ArgumentChecks.ensureNonNull("dashOffset", dashOffset);
+        this.dashOffset = dashOffset;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 47 * hash + Objects.hashCode(this.graphicFill);
+        hash = 47 * hash + Objects.hashCode(this.graphicStroke);
+        hash = 47 * hash + Objects.hashCode(this.color);
+        hash = 47 * hash + Objects.hashCode(this.opacity);
+        hash = 47 * hash + Objects.hashCode(this.width);
+        hash = 47 * hash + Objects.hashCode(this.lineJoin);
+        hash = 47 * hash + Objects.hashCode(this.lineCap);
+        hash = 47 * hash + Arrays.hashCode(this.dashArray);
+        hash = 47 * hash + Objects.hashCode(this.dashOffset);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Stroke other = (Stroke) obj;
+        if (!Objects.equals(this.graphicFill, other.graphicFill)) {
+            return false;
+        }
+        if (!Objects.equals(this.graphicStroke, other.graphicStroke)) {
+            return false;
+        }
+        if (!Objects.equals(this.color, other.color)) {
+            return false;
+        }
+        if (!Objects.equals(this.opacity, other.opacity)) {
+            return false;
+        }
+        if (!Objects.equals(this.width, other.width)) {
+            return false;
+        }
+        if (!Objects.equals(this.lineJoin, other.lineJoin)) {
+            return false;
+        }
+        if (!Objects.equals(this.lineCap, other.lineCap)) {
+            return false;
+        }
+        if (!Arrays.equals(this.dashArray, other.dashArray)) {
+            return false;
+        }
+        return Objects.equals(this.dashOffset, other.dashOffset);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Stroke castOrCopy(org.opengis.style.Stroke candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Stroke) {
+            return (Stroke) candidate;
+        }
+        return new Stroke(
+                GraphicFill.castOrCopy(candidate.getGraphicFill()),
+                GraphicStroke.castOrCopy(candidate.getGraphicStroke()),
+                candidate.getColor(),
+                candidate.getOpacity(),
+                candidate.getWidth(),
+                candidate.getLineJoin(),
+                candidate.getLineCap(),
+                candidate.getDashArray(),
+                candidate.getDashOffset());
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
new file mode 100644
index 0000000..bbfab32
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Style.java
@@ -0,0 +1,137 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import org.opengis.style.StyleVisitor;
+import org.opengis.style.Symbolizer;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Style}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Style implements org.opengis.style.Style {
+
+    private String name;
+    private Description description;
+    private boolean isDefault;
+    private final List<FeatureTypeStyle> fts = new ArrayList<>();
+    private Symbolizer defaultSymbolizer;
+
+    public Style() {
+    }
+
+    public Style(String name, Description description, boolean isDefault, List<FeatureTypeStyle> fts, Symbolizer defaultSymbolizer) {
+        this.name = name;
+        this.description = description;
+        this.isDefault = isDefault;
+        if (fts!=null) this.fts.addAll(fts);
+        this.defaultSymbolizer = defaultSymbolizer;
+    }
+
+    @Override
+    public List<FeatureTypeStyle> featureTypeStyles() {
+        return fts;
+    }
+
+    @Override
+    public boolean isDefault() {
+        return isDefault;
+    }
+
+    public void setDefault(boolean isDefault) {
+        this.isDefault = isDefault;
+    }
+
+    @Override
+    public Symbolizer getDefaultSpecification() {
+        return defaultSymbolizer;
+    }
+
+    public void setDefaultSpecification(Symbolizer defaultSymbolizer) {
+        this.defaultSymbolizer = defaultSymbolizer;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Description getDescription() {
+        return description;
+    }
+
+    public void setDescription(Description description) {
+        this.description = description;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 71 * hash + Objects.hashCode(this.name);
+        hash = 71 * hash + Objects.hashCode(this.description);
+        hash = 71 * hash + Objects.hashCode(this.fts);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Style other = (Style) obj;
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        return Objects.equals(this.fts, other.fts);
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static Style castOrCopy(org.opengis.style.Style candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof Style) {
+            return (Style) candidate;
+        }
+
+        final List<FeatureTypeStyle> cs = new ArrayList<>();
+        for (org.opengis.style.FeatureTypeStyle cr : candidate.featureTypeStyles()) {
+            cs.add(FeatureTypeStyle.castOrCopy(cr));
+        }
+        return new Style(
+                candidate.getName(),
+                Description.castOrCopy(candidate.getDescription()),
+                candidate.isDefault(),
+                cs,
+                org.apache.sis.internal.style.Symbolizer.tryCastOrCopy(candidate.getDefaultSpecification())
+                );
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java
new file mode 100644
index 0000000..d2bd4e2
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/StyleFactory.java
@@ -0,0 +1,550 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import javax.swing.Icon;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.measure.Units;
+import org.apache.sis.util.SimpleInternationalString;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory;
+import org.opengis.filter.Literal;
+import org.opengis.filter.ResourceId;
+import org.opengis.metadata.citation.OnlineResource;
+import org.opengis.style.ContrastMethod;
+import org.opengis.style.ExtensionSymbolizer;
+import org.opengis.style.OverlapBehavior;
+import org.opengis.style.SemanticType;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+
+/**
+ * {@link org.opengis.style.StyleFactory} implementation.
+ * Created objects are mutable.
+ *
+ * TODO : A proper review of GeoAPI Style API is required.
+ * This class and style objets implementations are drafts used to continue
+ * work on the rendering engine.
+ *
+ * @see StyleFactory
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class StyleFactory implements org.opengis.style.StyleFactory {
+
+    public static final SimpleInternationalString EMPTY_STRING = new SimpleInternationalString("");
+    public static final Literal<Feature,Double> LITERAL_ZERO;
+    public static final Literal<Feature,Double> LITERAL_HALF;
+    public static final Literal<Feature,Double> LITERAL_ONE;
+    public static final Literal<Feature,Color> LITERAL_WHITE;
+    public static final Literal<Feature,Color> LITERAL_GRAY;
+    public static final Literal<Feature,Color> LITERAL_BLACK;
+
+    public static final Literal<Feature,Double> DEFAULT_ANCHOR_POINT_X;
+    public static final Literal<Feature,Double> DEFAULT_ANCHOR_POINT_Y;
+
+    public static final ContrastMethod DEFAULT_CONTRAST_ENHANCEMENT_METHOD;
+    public static final Literal<Feature,Double> DEFAULT_CONTRAST_ENHANCEMENT_GAMMA;
+
+    public static final Literal<Feature,Double> DEFAULT_DISPLACEMENT_X;
+    public static final Literal<Feature,Double> DEFAULT_DISPLACEMENT_Y;
+
+    public static final Literal<Feature,Color> DEFAULT_FILL_COLOR;
+    public static final Literal<Feature,Double> DEFAULT_FILL_OPACITY;
+
+    public static final String STROKE_JOIN_MITRE_STRING = "mitre";
+    public static final String STROKE_JOIN_ROUND_STRING = "round";
+    public static final String STROKE_JOIN_BEVEL_STRING = "bevel";
+    public static final String STROKE_CAP_BUTT_STRING = "butt";
+    public static final String STROKE_CAP_ROUND_STRING = "round";
+    public static final String STROKE_CAP_SQUARE_STRING = "square";
+    public static final Literal<Feature,String> STROKE_JOIN_MITRE;
+    public static final Literal<Feature,String> STROKE_JOIN_ROUND;
+    public static final Literal<Feature,String> STROKE_JOIN_BEVEL;
+    public static final Literal<Feature,String> STROKE_CAP_BUTT;
+    public static final Literal<Feature,String> STROKE_CAP_ROUND;
+    public static final Literal<Feature,String> STROKE_CAP_SQUARE;
+    public static final Literal<Feature,Color> DEFAULT_STROKE_COLOR;
+    public static final Literal<Feature,Double> DEFAULT_STROKE_OPACITY;
+    public static final Literal<Feature,Double> DEFAULT_STROKE_WIDTH;
+    public static final Literal<Feature,String> DEFAULT_STROKE_JOIN;
+    public static final Literal<Feature,String> DEFAULT_STROKE_CAP;
+    public static final Literal<Feature,Double> DEFAULT_STROKE_OFFSET;
+
+    public static final String FONT_STYLE_NORMAL_STRING = "normal";
+    public static final String FONT_STYLE_ITALIC_STRING = "italic";
+    public static final String FONT_STYLE_OBLIQUE_STRING = "oblique";
+    public static final String FONT_WEIGHT_NORMAL_STRING = "normal";
+    public static final String FONT_WEIGHT_BOLD_STRING = "bold";
+    public static final Literal<Feature,String> FONT_STYLE_NORMAL;
+    public static final Literal<Feature,String> FONT_STYLE_ITALIC;
+    public static final Literal<Feature,String> FONT_STYLE_OBLIQUE;
+    public static final Literal<Feature,String> FONT_WEIGHT_NORMAL;
+    public static final Literal<Feature,String> FONT_WEIGHT_BOLD;
+    public static final Literal<Feature,String> DEFAULT_FONT_STYLE;
+    public static final Literal<Feature,String> DEFAULT_FONT_WEIGHT;
+    public static final Literal<Feature,Double> DEFAULT_FONT_SIZE;
+
+    public static final Literal<Feature,Double> DEFAULT_HALO_RADIUS;
+
+    public static final String MARK_SQUARE_STRING = "square";
+    public static final String MARK_CIRCLE_STRING = "circle";
+    public static final String MARK_TRIANGLE_STRING = "triangle";
+    public static final String MARK_STAR_STRING = "star";
+    public static final String MARK_CROSS_STRING = "cross";
+    public static final String MARK_X_STRING = "x";
+    public static final Literal<Feature,String> MARK_SQUARE;
+    public static final Literal<Feature,String> MARK_CIRCLE;
+    public static final Literal<Feature,String> MARK_TRIANGLE;
+    public static final Literal<Feature,String> MARK_STAR;
+    public static final Literal<Feature,String> MARK_CROSS;
+    public static final Literal<Feature,String> MARK_X;
+    public static final Literal<Feature,String> DEFAULT_MARK_WKN;
+
+    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_OPACITY;
+    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_ROTATION;
+    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_SIZE;
+    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_STROKE_INITIAL_GAP;
+    public static final Literal<Feature,Double> DEFAULT_GRAPHIC_STROKE_GAP;
+
+    public static final Unit<Length> DEFAULT_UOM;
+    public static final String DEFAULT_GEOM;
+
+    public static final Literal<Feature,String> DEFAULT_TEXT_LABEL;
+
+    static {
+        final FilterFactory<Feature,Object,Object> FF = DefaultFilterFactory.forFeatures();
+        LITERAL_ZERO = FF.literal(0.0);
+        LITERAL_HALF = FF.literal(0.5);
+        LITERAL_ONE = FF.literal(1.0);
+        LITERAL_WHITE = FF.literal(Color.WHITE);
+        LITERAL_GRAY = FF.literal(Color.GRAY);
+        LITERAL_BLACK = FF.literal(Color.BLACK);
+
+        DEFAULT_UOM = Units.POINT;
+        DEFAULT_GEOM = null;
+
+        DEFAULT_ANCHOR_POINT_X = LITERAL_HALF;
+        DEFAULT_ANCHOR_POINT_Y = LITERAL_HALF;
+
+        DEFAULT_CONTRAST_ENHANCEMENT_METHOD = ContrastMethod.NONE;
+        DEFAULT_CONTRAST_ENHANCEMENT_GAMMA = LITERAL_ONE;
+
+        DEFAULT_DISPLACEMENT_X = LITERAL_ZERO;
+        DEFAULT_DISPLACEMENT_Y = LITERAL_ZERO;
+
+        DEFAULT_FILL_COLOR = FF.literal(Color.GRAY);
+        DEFAULT_FILL_OPACITY = LITERAL_ONE;
+
+        STROKE_JOIN_MITRE = FF.literal(STROKE_JOIN_MITRE_STRING);
+        STROKE_JOIN_ROUND = FF.literal(STROKE_JOIN_ROUND_STRING);
+        STROKE_JOIN_BEVEL = FF.literal(STROKE_JOIN_BEVEL_STRING);
+        STROKE_CAP_BUTT = FF.literal(STROKE_CAP_BUTT_STRING);
+        STROKE_CAP_ROUND = FF.literal(STROKE_CAP_ROUND_STRING);
+        STROKE_CAP_SQUARE = FF.literal(STROKE_CAP_SQUARE_STRING);
+        DEFAULT_STROKE_COLOR = FF.literal(Color.BLACK);
+        DEFAULT_STROKE_OPACITY = LITERAL_ONE;
+        DEFAULT_STROKE_WIDTH = LITERAL_ONE;
+        DEFAULT_STROKE_JOIN = STROKE_JOIN_BEVEL;
+        DEFAULT_STROKE_CAP = STROKE_CAP_SQUARE;
+        DEFAULT_STROKE_OFFSET = LITERAL_ZERO;
+
+        FONT_STYLE_NORMAL = FF.literal(FONT_STYLE_NORMAL_STRING);
+        FONT_STYLE_ITALIC = FF.literal(FONT_STYLE_ITALIC_STRING);
+        FONT_STYLE_OBLIQUE = FF.literal(FONT_STYLE_OBLIQUE_STRING);
+        FONT_WEIGHT_NORMAL = FF.literal(FONT_WEIGHT_NORMAL_STRING);
+        FONT_WEIGHT_BOLD = FF.literal(FONT_WEIGHT_BOLD_STRING);
+        DEFAULT_FONT_STYLE = FONT_STYLE_NORMAL;
+        DEFAULT_FONT_WEIGHT = FONT_WEIGHT_NORMAL;
+        DEFAULT_FONT_SIZE = FF.literal(10.0);
+
+        DEFAULT_HALO_RADIUS = LITERAL_ONE;
+
+        MARK_SQUARE = FF.literal(MARK_SQUARE_STRING);
+        MARK_CIRCLE = FF.literal(MARK_CIRCLE_STRING);
+        MARK_TRIANGLE = FF.literal(MARK_TRIANGLE_STRING);
+        MARK_STAR = FF.literal(MARK_STAR_STRING);
+        MARK_CROSS = FF.literal(MARK_CROSS_STRING);
+        MARK_X = FF.literal(MARK_X_STRING);
+        DEFAULT_MARK_WKN = MARK_SQUARE;
+
+        DEFAULT_GRAPHIC_STROKE_INITIAL_GAP = LITERAL_ZERO;
+        DEFAULT_GRAPHIC_STROKE_GAP = LITERAL_ZERO;
+
+        DEFAULT_GRAPHIC_OPACITY = LITERAL_ONE;
+        DEFAULT_GRAPHIC_ROTATION = LITERAL_ZERO;
+        DEFAULT_GRAPHIC_SIZE = FF.literal(6.0);
+
+        DEFAULT_TEXT_LABEL = FF.literal("Label");
+
+    }
+
+    public StyleFactory(){}
+
+    @Override
+    public AnchorPoint anchorPoint(Expression x, Expression y) {
+        return new org.apache.sis.internal.style.AnchorPoint(x, y);
+    }
+
+    @Override
+    public ChannelSelection channelSelection(org.opengis.style.SelectedChannelType gray) {
+        return new ChannelSelection(SelectedChannelType.castOrCopy(gray));
+    }
+
+    @Override
+    public ChannelSelection channelSelection(
+            org.opengis.style.SelectedChannelType red,
+            org.opengis.style.SelectedChannelType green,
+            org.opengis.style.SelectedChannelType blue) {
+        return new ChannelSelection(
+                SelectedChannelType.castOrCopy(red),
+                SelectedChannelType.castOrCopy(green),
+                SelectedChannelType.castOrCopy(blue));
+    }
+
+    @Override
+    public ColorMap colorMap(Expression propertyName, Expression... mapping) {
+        return new ColorMap(propertyName);
+    }
+
+    @Override
+    public ColorReplacement colorReplacement(Expression propertyName, Expression... mapping) {
+        return new ColorReplacement(propertyName);
+    }
+
+    @Override
+    public ContrastEnhancement contrastEnhancement(Expression gamma, ContrastMethod method) {
+        return new ContrastEnhancement(method, gamma);
+    }
+
+    @Override
+    public Description description(InternationalString title, InternationalString description) {
+        return new Description(title, description);
+    }
+
+    @Override
+    public Displacement displacement(Expression dx, Expression dy) {
+        return new Displacement(dx, dy);
+    }
+
+    @Override
+    public ExternalGraphic externalGraphic(OnlineResource resource, String format,
+            Collection<org.opengis.style.ColorReplacement> replacements) {
+
+        List<ColorReplacement> cs = null;
+        if (replacements != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.ColorReplacement cr : replacements) {
+                cs.add(ColorReplacement.castOrCopy(cr));
+            }
+        }
+        return new ExternalGraphic(resource, null, format, cs);
+    }
+
+    @Override
+    public ExternalGraphic externalGraphic(Icon inline,
+            Collection<org.opengis.style.ColorReplacement> replacements) {
+        List<ColorReplacement> cs = null;
+        if (replacements != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.ColorReplacement cr : replacements) {
+                cs.add(ColorReplacement.castOrCopy(cr));
+            }
+        }
+        return new ExternalGraphic(null, inline, null, cs);
+    }
+
+    @Override
+    public ExternalMark externalMark(OnlineResource resource, String format, int markIndex) {
+        return new ExternalMark(resource, null, format, markIndex);
+    }
+
+    @Override
+    public ExternalMark externalMark(Icon inline) {
+        return new ExternalMark(null, inline, null, 0);
+    }
+
+    @Override
+    public FeatureTypeStyle featureTypeStyle(String name,
+            org.opengis.style.Description description,
+            ResourceId definedFor, Set<GenericName> featureTypeNames,
+            Set<SemanticType> types, List<org.opengis.style.Rule> rules) {
+        List<Rule> cs = null;
+        if (rules != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.Rule cr : rules) {
+                cs.add(Rule.castOrCopy(cr));
+            }
+        }
+        return new FeatureTypeStyle(name, Description.castOrCopy(description),
+                definedFor, featureTypeNames, types, cs, null);
+    }
+
+    @Override
+    public Fill fill(org.opengis.style.GraphicFill fill, Expression color, Expression opacity) {
+        return new Fill(GraphicFill.castOrCopy(fill), color, opacity);
+    }
+
+    @Override
+    public Font font(List<Expression> family, Expression style, Expression weight, Expression size) {
+        return new Font(family, style, weight, size);
+    }
+
+    @Override
+    public Graphic graphic( List<org.opengis.style.GraphicalSymbol> symbols,
+            Expression opacity, Expression size, Expression rotation,
+            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
+        List<GraphicalSymbol> cs = null;
+        if (symbols != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.GraphicalSymbol cr : symbols) {
+                cs.add(GraphicalSymbol.castOrCopy(cr));
+            }
+        }
+        return new Graphic(cs, opacity, size, rotation,
+                AnchorPoint.castOrCopy(anchor),
+                Displacement.castOrCopy(disp));
+    }
+
+    @Override
+    public GraphicFill graphicFill(List<org.opengis.style.GraphicalSymbol> symbols,
+            Expression opacity, Expression size, Expression rotation,
+            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
+        List<GraphicalSymbol> cs = null;
+        if (symbols != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.GraphicalSymbol cr : symbols) {
+                cs.add(GraphicalSymbol.castOrCopy(cr));
+            }
+        }
+        return new GraphicFill(cs, opacity, size, rotation,
+                AnchorPoint.castOrCopy(anchor),
+                Displacement.castOrCopy(disp));
+    }
+
+    @Override
+    public GraphicLegend graphicLegend(List<org.opengis.style.GraphicalSymbol> symbols,
+            Expression opacity, Expression size, Expression rotation,
+            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp) {
+        List<GraphicalSymbol> cs = null;
+        if (symbols != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.GraphicalSymbol cr : symbols) {
+                cs.add(GraphicalSymbol.castOrCopy(cr));
+            }
+        }
+        return new GraphicLegend(cs, opacity, size, rotation,
+                AnchorPoint.castOrCopy(anchor),
+                Displacement.castOrCopy(disp));
+    }
+
+    @Override
+    public GraphicStroke graphicStroke(List<org.opengis.style.GraphicalSymbol> symbols,
+            Expression opacity, Expression size, Expression rotation,
+            org.opengis.style.AnchorPoint anchor, org.opengis.style.Displacement disp,
+            Expression initialGap, Expression gap) {
+        List<GraphicalSymbol> cs = null;
+        if (symbols != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.GraphicalSymbol cr : symbols) {
+                cs.add(GraphicalSymbol.castOrCopy(cr));
+            }
+        }
+        return new GraphicStroke(cs, opacity, size, rotation,
+                AnchorPoint.castOrCopy(anchor),
+                Displacement.castOrCopy(disp),
+                initialGap, gap);
+    }
+
+    @Override
+    public Halo halo(org.opengis.style.Fill fill, Expression radius) {
+        return new Halo(Fill.castOrCopy(fill), radius);
+    }
+
+    @Override
+    public LinePlacement linePlacement(Expression offset, Expression initialGap,
+            Expression gap, boolean repeated, boolean aligned, boolean generalizedLine) {
+        return new LinePlacement(offset, initialGap, gap, repeated, aligned, generalizedLine);
+    }
+
+    @Override
+    public LineSymbolizer lineSymbolizer(String name, Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit,
+            org.opengis.style.Stroke stroke, Expression offset) {
+        return new LineSymbolizer(name, geometry,
+                Description.castOrCopy(description),
+                (Unit<Length>) unit,
+                Stroke.castOrCopy(stroke), offset);
+    }
+
+    @Override
+    public Mark mark(Expression wellKnownName, org.opengis.style.Fill fill,
+            org.opengis.style.Stroke stroke) {
+        return new Mark(wellKnownName, null,
+                Fill.castOrCopy(fill),
+                Stroke.castOrCopy(stroke));
+    }
+
+    @Override
+    public Mark mark(org.opengis.style.ExternalMark externalMark,
+            org.opengis.style.Fill fill, org.opengis.style.Stroke stroke) {
+        return new Mark(null, ExternalMark.castOrCopy(externalMark),
+                Fill.castOrCopy(fill),
+                Stroke.castOrCopy(stroke));
+    }
+
+    @Override
+    public PointPlacement pointPlacement(org.opengis.style.AnchorPoint anchor,
+            org.opengis.style.Displacement displacement, Expression rotation) {
+        return new PointPlacement(
+                AnchorPoint.castOrCopy(anchor),
+                Displacement.castOrCopy(displacement),
+                rotation);
+    }
+
+    @Override
+    public PointSymbolizer pointSymbolizer(String name, Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit,
+            org.opengis.style.Graphic graphic) {
+        return new PointSymbolizer(name, geometry,
+                Description.castOrCopy(description), (Unit<Length>) unit,
+                Graphic.castOrCopy(graphic));
+    }
+
+    @Override
+    public PolygonSymbolizer polygonSymbolizer(String name, Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit,
+            org.opengis.style.Stroke stroke, org.opengis.style.Fill fill,
+            org.opengis.style.Displacement displacement, Expression offset) {
+        return new PolygonSymbolizer(name, geometry,
+                Description.castOrCopy(description), (Unit<Length>) unit,
+                Stroke.castOrCopy(stroke),
+                Fill.castOrCopy(fill),
+                Displacement.castOrCopy(displacement), offset);
+    }
+
+    @Override
+    public RasterSymbolizer rasterSymbolizer(String name, Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit,
+            Expression opacity, org.opengis.style.ChannelSelection channelSelection,
+            OverlapBehavior overlapsBehaviour,
+            org.opengis.style.ColorMap colorMap,
+            org.opengis.style.ContrastEnhancement contrast,
+            org.opengis.style.ShadedRelief shaded,
+            org.opengis.style.Symbolizer outline) {
+        return new RasterSymbolizer(name, geometry,
+                Description.castOrCopy(description),
+                (Unit<Length>) unit, opacity,
+                ChannelSelection.castOrCopy(channelSelection),
+                overlapsBehaviour,
+                ColorMap.castOrCopy(colorMap),
+                ContrastEnhancement.castOrCopy(contrast),
+                ShadedRelief.castOrCopy(shaded),
+                outline);
+    }
+
+    @Override
+    public ExtensionSymbolizer extensionSymbolizer(String name, String geometry,
+            org.opengis.style.Description description, Unit<?> unit, String extensionName,
+            Map<String, Expression> parameters) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public ExtensionSymbolizer extensionSymbolizer(String name, Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit, String extensionName,
+            Map<String, Expression> parameters) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public Rule rule(String name, org.opengis.style.Description description,
+            org.opengis.style.GraphicLegend legend, double min, double max,
+            List<org.opengis.style.Symbolizer> symbolizers, Filter filter) {
+        return new Rule(name,
+                Description.castOrCopy(description),
+                GraphicLegend.castOrCopy(legend),
+                filter, false, min, max, symbolizers, null);
+    }
+
+    @Override
+    public SelectedChannelType selectedChannelType(String channelName,
+            org.opengis.style.ContrastEnhancement contrastEnhancement) {
+        return new SelectedChannelType(channelName,
+                ContrastEnhancement.castOrCopy(contrastEnhancement));
+    }
+
+    @Override
+    public ShadedRelief shadedRelief(Expression reliefFactor, boolean brightnessOnly) {
+        return new ShadedRelief(brightnessOnly, reliefFactor);
+    }
+
+    @Override
+    public Stroke stroke(Expression color, Expression opacity, Expression width,
+            Expression join, Expression cap, float[] dashes, Expression offset) {
+        return new Stroke(null, null, color, opacity, width, join, cap, dashes, offset);
+    }
+
+    @Override
+    public Stroke stroke(org.opengis.style.GraphicFill fill, Expression color,
+            Expression opacity, Expression width, Expression join, Expression cap,
+            float[] dashes, Expression offset) {
+        return new Stroke(GraphicFill.castOrCopy(fill),
+                null, color, opacity, width, join, cap, dashes, offset);
+    }
+
+    @Override
+    public Stroke stroke(org.opengis.style.GraphicStroke stroke,
+            Expression color, Expression opacity, Expression width,
+            Expression join, Expression cap, float[] dashes, Expression offset) {
+        return new Stroke(null, GraphicStroke.castOrCopy(stroke),
+                color, opacity, width, join, cap, dashes, offset);
+    }
+
+    @Override
+    public Style style(String name, org.opengis.style.Description description,
+            boolean isDefault, List<org.opengis.style.FeatureTypeStyle> featureTypeStyles,
+            org.opengis.style.Symbolizer defaultSymbolizer) {
+        List<FeatureTypeStyle> cs = null;
+        if (featureTypeStyles != null) {
+            cs = new ArrayList<>();
+            for (org.opengis.style.FeatureTypeStyle cr : featureTypeStyles) {
+                cs.add(FeatureTypeStyle.castOrCopy(cr));
+            }
+        }
+        return new Style(
+                name,
+                Description.castOrCopy(description),
+                isDefault,
+                cs,
+                defaultSymbolizer);
+    }
+
+    @Override
+    public TextSymbolizer textSymbolizer(String name,Expression geometry,
+            org.opengis.style.Description description, Unit<?> unit,
+            Expression label, org.opengis.style.Font font,
+            org.opengis.style.LabelPlacement placement,
+            org.opengis.style.Halo halo,
+            org.opengis.style.Fill fill) {
+        return new TextSymbolizer(name, geometry,
+                Description.castOrCopy(description),
+                (Unit<Length>) unit, label,
+                Font.castOrCopy(font),
+                LabelPlacement.castOrCopy(placement),
+                Halo.castOrCopy(halo),
+                Fill.castOrCopy(fill));
+    }
+
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
new file mode 100644
index 0000000..9df6b5b
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/Symbolizer.java
@@ -0,0 +1,126 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Objects;
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.Description;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.Symbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public abstract class Symbolizer implements org.opengis.style.Symbolizer {
+
+    private String name;
+    private Description description;
+    private Expression<Feature,?> geometry;
+    private Unit<Length> unit;
+
+    public Symbolizer() {
+    }
+
+    public Symbolizer(String name, Expression geometry, Description description, Unit<Length> unit) {
+        this.name = name;
+        this.geometry = geometry;
+        this.description = description;
+        this.unit = unit;
+    }
+
+    @Override
+    public Unit<Length> getUnitOfMeasure() {
+        return unit;
+    }
+
+    public void setUnitOfMeasure(Unit<Length> unit) {
+        this.unit = unit;
+    }
+
+    @Override
+    public String getGeometryPropertyName() {
+        throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody
+    }
+
+    @Override
+    public Expression<Feature, ?> getGeometry() {
+        return geometry;
+    }
+
+    public void setGeometry(Expression<Feature, ?> geometry) {
+        this.geometry = geometry;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Description getDescription() {
+        return description;
+    }
+
+    public void setDescription(Description description) {
+        this.description = description;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 97 * hash + Objects.hashCode(this.name);
+        hash = 97 * hash + Objects.hashCode(this.description);
+        hash = 97 * hash + Objects.hashCode(this.geometry);
+        hash = 97 * hash + Objects.hashCode(this.unit);
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final Symbolizer other = (Symbolizer) obj;
+        if (!Objects.equals(this.name, other.name)) {
+            return false;
+        }
+        if (!Objects.equals(this.description, other.description)) {
+            return false;
+        }
+        if (!Objects.equals(this.geometry, other.geometry)) {
+            return false;
+        }
+        return Objects.equals(this.unit, other.unit);
+    }
+
+    static org.opengis.style.Symbolizer tryCastOrCopy(org.opengis.style.Symbolizer s) {
+        if (s instanceof org.opengis.style.PointSymbolizer) {
+            return PointSymbolizer.castOrCopy((org.opengis.style.PointSymbolizer) s);
+        } else if (s instanceof org.opengis.style.LineSymbolizer) {
+            return LineSymbolizer.castOrCopy((org.opengis.style.LineSymbolizer) s);
+        } else if (s instanceof org.opengis.style.PolygonSymbolizer) {
+            return PolygonSymbolizer.castOrCopy((org.opengis.style.PolygonSymbolizer) s);
+        } else if (s instanceof org.opengis.style.TextSymbolizer) {
+            return TextSymbolizer.castOrCopy((org.opengis.style.TextSymbolizer) s);
+        } else if (s instanceof org.opengis.style.RasterSymbolizer) {
+            return RasterSymbolizer.castOrCopy((org.opengis.style.RasterSymbolizer) s);
+        } else {
+            //leave it unchanged
+            return s;
+        }
+    }
+}
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java
new file mode 100644
index 0000000..c8a0613
--- /dev/null
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/style/TextSymbolizer.java
@@ -0,0 +1,123 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Length;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.style.StyleVisitor;
+
+/**
+ * Mutable implementation of {@link org.opengis.style.TextSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class TextSymbolizer extends Symbolizer implements org.opengis.style.TextSymbolizer {
+
+    private Expression<Feature,String> label;
+    private Font font;
+    private LabelPlacement labelPlacement;
+    private Halo halo;
+    private Fill fill;
+
+    public static TextSymbolizer createDefault() {
+        return new TextSymbolizer(null, null, new Description(), StyleFactory.DEFAULT_UOM,
+            StyleFactory.DEFAULT_TEXT_LABEL, new Font(),
+                new PointPlacement(new AnchorPoint(), new Displacement(), StyleFactory.LITERAL_ZERO),
+                null, new Fill());
+    }
+
+    public TextSymbolizer() {
+    }
+
+    public TextSymbolizer(String name, Expression geometry,
+            Description description,
+            Unit<Length> unit,
+            Expression label,
+            Font font,
+            LabelPlacement placement,
+            Halo halo,
+            Fill fill) {
+        super(name, geometry, description, unit);
+        this.label = label;
+        this.font = font;
+        this.labelPlacement = placement;
+        this.halo = halo;
+        this.fill = fill;
+    }
+
+    @Override
+    public Object accept(StyleVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    @Override
+    public Expression<Feature,String> getLabel() {
+        return label;
+    }
+
+    public void setLabel(Expression<Feature, String> label) {
+        this.label = label;
+    }
+
+    @Override
+    public Font getFont() {
+        return font;
+    }
+
+    public void setFont(Font font) {
+        this.font = font;
+    }
+
+    @Override
+    public LabelPlacement getLabelPlacement() {
+        return labelPlacement;
+    }
+
+    public void setLabelPlacement(LabelPlacement labelPlacement) {
+        this.labelPlacement = labelPlacement;
+    }
+
+    @Override
+    public Halo getHalo() {
+        return halo;
+    }
+
+    public void setHalo(Halo halo) {
+        this.halo = halo;
+    }
+
+    @Override
+    public Fill getFill() {
+        return fill;
+    }
+
+    public void setFill(Fill fill) {
+        this.fill = fill;
+    }
+
+    /**
+     * Cast or copy to an SIS implementation.
+     *
+     * @param candidate to copy, can be null.
+     * @return cast or copied object.
+     */
+    public static TextSymbolizer castOrCopy(org.opengis.style.TextSymbolizer candidate) {
+        if (candidate == null) {
+            return null;
+        } else if (candidate instanceof TextSymbolizer) {
+            return (TextSymbolizer) candidate;
+        }
+        return new TextSymbolizer(candidate.getName(),
+                candidate.getGeometry(),
+                org.apache.sis.internal.style.Description.castOrCopy(candidate.getDescription()),
+                candidate.getUnitOfMeasure(),
+                candidate.getLabel(),
+                org.apache.sis.internal.style.Font.castOrCopy(candidate.getFont()),
+                LabelPlacement.castOrCopy(candidate.getLabelPlacement()),
+                org.apache.sis.internal.style.Halo.castOrCopy(candidate.getHalo()),
+                org.apache.sis.internal.style.Fill.castOrCopy(candidate.getFill()));
+    }
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockFeatureTypeStyle.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockFeatureTypeStyle.java
deleted file mode 100644
index fdd5cf3..0000000
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockFeatureTypeStyle.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.internal.map;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import org.opengis.filter.ResourceId;
-import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.Description;
-import org.opengis.style.FeatureTypeStyle;
-import org.opengis.style.Rule;
-import org.opengis.style.SemanticType;
-import org.opengis.style.StyleVisitor;
-import org.opengis.util.GenericName;
-
-/**
- *
- * @author Johann Sorel (Geomatys)
- */
-public final class MockFeatureTypeStyle implements FeatureTypeStyle {
-
-    private String name;
-    private Description description;
-    private final Set<GenericName> featureTypeNames = new HashSet<>();
-    private final Set<SemanticType> semanticTypes = new HashSet<>();
-    private final List<Rule> rules = new ArrayList<>();
-    private OnlineResource onlineResource;
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public Description getDescription() {
-        return description;
-    }
-
-    public void setDescription(Description description) {
-        this.description = description;
-    }
-
-    @Override
-    public Set<GenericName> featureTypeNames() {
-        return featureTypeNames;
-    }
-
-    @Override
-    public Set<SemanticType> semanticTypeIdentifiers() {
-        return semanticTypes;
-    }
-
-    @Override
-    public List<Rule> rules() {
-        return rules;
-    }
-
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
-    }
-
-    public void setOnlineResource(OnlineResource onlineResource) {
-        this.onlineResource = onlineResource;
-    }
-
-    /**
-     * May be removed from GeoAPI.
-     */
-    @Override
-    @Deprecated
-    public ResourceId getFeatureInstanceIDs() {
-        throw new UnsupportedOperationException();
-    }
-
-    /**
-     * May be removed from GeoAPI.
-     */
-    @Override
-    @Deprecated
-    public Object accept(StyleVisitor visitor, Object extraData) {
-        throw new UnsupportedOperationException();
-    }
-}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockLineSymbolizer.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockLineSymbolizer.java
deleted file mode 100644
index f9df4e7..0000000
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockLineSymbolizer.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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.internal.map;
-
-import javax.measure.Unit;
-import javax.measure.quantity.Length;
-import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
-import org.opengis.style.Description;
-import org.opengis.style.LineSymbolizer;
-import org.opengis.style.Stroke;
-import org.opengis.style.StyleVisitor;
-
-/**
- *
- * @author Johann Sorel (Geomatys)
- */
-public final class MockLineSymbolizer implements LineSymbolizer {
-
-    String name;
-    Description description;
-
-    Stroke stroke;
-    Expression<Feature,?> perpendicularOffset;
-    Unit<Length> unitOfMeasure;
-    Expression<Feature,?> geometry;
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public Description getDescription() {
-        return description;
-    }
-
-    public void setDescription(Description description) {
-        this.description = description;
-    }
-
-    @Override
-    public Stroke getStroke() {
-        return stroke;
-    }
-
-    public void setStroke(Stroke stroke) {
-        this.stroke = stroke;
-    }
-
-    @Override
-    public Expression getPerpendicularOffset() {
-        return perpendicularOffset;
-    }
-
-    public void setPerpendicularOffset(Expression perpendicularOffset) {
-        this.perpendicularOffset = perpendicularOffset;
-    }
-
-    @Override
-    public Unit<Length> getUnitOfMeasure() {
-        return unitOfMeasure;
-    }
-
-    public void setUnitOfMeasure(Unit<Length> unitOfMeasure) {
-        this.unitOfMeasure = unitOfMeasure;
-    }
-
-    @Override
-    public Expression<Feature,?> getGeometry() {
-        return geometry;
-    }
-
-    public void setGeometry(Expression geometry) {
-        this.geometry = geometry;
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public String getGeometryPropertyName() {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public Object accept(StyleVisitor sv, Object o) {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java
deleted file mode 100644
index f3eecee..0000000
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockRule.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.internal.map;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.opengis.filter.Filter;
-import org.opengis.metadata.citation.OnlineResource;
-import org.opengis.style.Description;
-import org.opengis.style.GraphicLegend;
-import org.opengis.style.Rule;
-import org.opengis.style.StyleVisitor;
-import org.opengis.style.Symbolizer;
-
-/**
- *
- * @author Johann Sorel (Geomatys)
- */
-public final class MockRule implements Rule {
-
-    private String name;
-    private Description description;
-    private GraphicLegend legend;
-    private Filter filter;
-    private boolean iselseFilter;
-    private double minScaleDenominator = 0.0;
-    private double maxScaleDenominator = Double.MAX_VALUE;
-    private final List<Symbolizer> symbolizers = new ArrayList<>();
-    private OnlineResource onlineResource;
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public Description getDescription() {
-        return description;
-    }
-
-    public void setDescription(Description description) {
-        this.description = description;
-    }
-
-    @Override
-    public GraphicLegend getLegend() {
-        return legend;
-    }
-
-    public void setLegend(GraphicLegend legend) {
-        this.legend = legend;
-    }
-
-    @Override
-    public Filter getFilter() {
-        return filter;
-    }
-
-    public void setFilter(Filter filter) {
-        this.filter = filter;
-    }
-
-    @Override
-    public boolean isElseFilter() {
-        return iselseFilter;
-    }
-
-    public void setIsElseFilter(boolean iselseFilter) {
-        this.iselseFilter = iselseFilter;
-    }
-
-    @Override
-    public double getMinScaleDenominator() {
-        return minScaleDenominator;
-    }
-
-    public void setMinScaleDenominator(double minScaleDenominator) {
-        this.minScaleDenominator = minScaleDenominator;
-    }
-
-    @Override
-    public double getMaxScaleDenominator() {
-        return maxScaleDenominator;
-    }
-
-    public void setMaxScaleDenominator(double maxScaleDenominator) {
-        this.maxScaleDenominator = maxScaleDenominator;
-    }
-
-    @Override
-    public List<Symbolizer> symbolizers() {
-        return symbolizers;
-    }
-
-    @Override
-    public OnlineResource getOnlineResource() {
-        return onlineResource;
-    }
-
-    public void setOnlineResource(OnlineResource onlineResource) {
-        this.onlineResource = onlineResource;
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public Object accept(StyleVisitor sv, Object o) {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockStyle.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockStyle.java
deleted file mode 100644
index 2109949..0000000
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/MockStyle.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.internal.map;
-
-import java.util.ArrayList;
-import java.util.List;
-import org.opengis.style.Description;
-import org.opengis.style.FeatureTypeStyle;
-import org.opengis.style.StyleVisitor;
-import org.opengis.style.Symbolizer;
-
-/**
- *
- * @author Johann Sorel (Geomatys)
- */
-public final class MockStyle implements org.opengis.style.Style {
-
-    private String name;
-    private Description description;
-    private final List<FeatureTypeStyle> fts = new ArrayList<>();
-
-    @Override
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    @Override
-    public Description getDescription() {
-        return description;
-    }
-
-    public void setDescription(Description description) {
-        this.description = description;
-    }
-
-    @Override
-    public List<FeatureTypeStyle> featureTypeStyles() {
-        return fts;
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public boolean isDefault() {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public Symbolizer getDefaultSpecification() {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-    /**
-     * Will likely be removed from geoapi.
-     */
-    @Deprecated
-    @Override
-    public Object accept(StyleVisitor sv, Object o) {
-        throw new UnsupportedOperationException("Not supported.");
-    }
-
-}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
index a946955..feb7440 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/map/SEPortrayerTest.java
@@ -35,6 +35,10 @@ import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.MemoryFeatureSet;
+import org.apache.sis.internal.style.FeatureTypeStyle;
+import org.apache.sis.internal.style.LineSymbolizer;
+import org.apache.sis.internal.style.Rule;
+import org.apache.sis.internal.style.Style;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.portrayal.MapItem;
 import org.apache.sis.portrayal.MapLayer;
@@ -58,6 +62,7 @@ import org.locationtech.jts.geom.Polygon;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.filter.BinaryComparisonOperator;
+import org.opengis.filter.Expression;
 import org.opengis.filter.Filter;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.MatchAction;
@@ -185,10 +190,10 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testSanity() {
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
-        final MockRule rule = new MockRule();
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
+        final Rule rule = new Rule();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -222,10 +227,10 @@ public class SEPortrayerTest extends TestCase {
         env.setRange(0, 9, 11);
         env.setRange(1, 19, 21);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
-        final MockRule rule = new MockRule();
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
+        final Rule rule = new Rule();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -251,10 +256,10 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testUserQuery() {
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
-        final MockRule rule = new MockRule();
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
+        final Rule rule = new Rule();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -289,11 +294,11 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testFeatureTypeStyleTypeNames() {
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         fts.featureTypeNames().add(Names.createLocalName(null, null, "boat"));
-        final MockRule rule = new MockRule();
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final Rule rule = new Rule();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -320,11 +325,11 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testFeatureTypeStyleSemanticType() {
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         fts.semanticTypeIdentifiers().add(SemanticType.POINT);
-        final MockRule rule = new MockRule();
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final Rule rule = new Rule();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -354,11 +359,11 @@ public class SEPortrayerTest extends TestCase {
     public void testRuleFilter() {
         final Filter<Feature> filter = filterFactory.resourceId("2");
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
-        final MockRule rule = new MockRule();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
+        final Rule rule = new Rule();
         rule.setFilter(filter);
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -384,26 +389,26 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testRuleScale() {
-        final MockLineSymbolizer symbolizerAbove = new MockLineSymbolizer();
-        final MockLineSymbolizer symbolizerUnder = new MockLineSymbolizer();
-        final MockLineSymbolizer symbolizerMatch = new MockLineSymbolizer();
+        final LineSymbolizer symbolizerAbove = new LineSymbolizer();
+        final LineSymbolizer symbolizerUnder = new LineSymbolizer();
+        final LineSymbolizer symbolizerMatch = new LineSymbolizer();
 
         //Symbology rendering scale here is 3.944391406060875E8
-        final MockRule ruleAbove = new MockRule();
+        final Rule ruleAbove = new Rule();
         ruleAbove.symbolizers().add(symbolizerAbove);
         ruleAbove.setMinScaleDenominator(4e8);
         ruleAbove.setMaxScaleDenominator(Double.MAX_VALUE);
-        final MockRule ruleUnder = new MockRule();
+        final Rule ruleUnder = new Rule();
         ruleUnder.symbolizers().add(symbolizerUnder);
         ruleUnder.setMinScaleDenominator(0.0);
         ruleUnder.setMaxScaleDenominator(3e8);
-        final MockRule ruleMatch = new MockRule();
+        final Rule ruleMatch = new Rule();
         ruleMatch.symbolizers().add(symbolizerMatch);
         ruleMatch.setMinScaleDenominator(3e8);
         ruleMatch.setMaxScaleDenominator(4e8);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(ruleAbove);
         fts.rules().add(ruleUnder);
@@ -440,11 +445,11 @@ public class SEPortrayerTest extends TestCase {
                 filterFactory.literal("White Shark"),
                 true, MatchAction.ANY);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
-        final MockRule rule = new MockRule();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
+        final Rule rule = new Rule();
         rule.setFilter(filter);
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
         rule.symbolizers().add(symbolizer);
@@ -474,18 +479,18 @@ public class SEPortrayerTest extends TestCase {
     public void testRuleElseCondition() {
         final Filter<Feature> filter = filterFactory.resourceId("10");
 
-        final MockLineSymbolizer symbolizerBase = new MockLineSymbolizer();
-        final MockLineSymbolizer symbolizerElse = new MockLineSymbolizer();
+        final LineSymbolizer symbolizerBase = new LineSymbolizer();
+        final LineSymbolizer symbolizerElse = new LineSymbolizer();
 
-        final MockRule ruleBase = new MockRule();
+        final Rule ruleBase = new Rule();
         ruleBase.symbolizers().add(symbolizerBase);
         ruleBase.setFilter(filter);
-        final MockRule ruleOther = new MockRule();
-        ruleOther.setIsElseFilter(true);
+        final Rule ruleOther = new Rule();
+        ruleOther.setElseFilter(true);
         ruleOther.symbolizers().add(symbolizerElse);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(ruleBase);
         fts.rules().add(ruleOther);
@@ -515,13 +520,13 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testAggregateResource() {
-        final MockLineSymbolizer symbolizerBase = new MockLineSymbolizer();
+        final LineSymbolizer symbolizerBase = new LineSymbolizer();
 
-        final MockRule ruleBase = new MockRule();
+        final Rule ruleBase = new Rule();
         ruleBase.symbolizers().add(symbolizerBase);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(ruleBase);
 
@@ -570,14 +575,14 @@ public class SEPortrayerTest extends TestCase {
     @Test
     public void testPreserveProperties() {
         final Filter<Feature> filter = filterFactory.resourceId("2");
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
+        final LineSymbolizer symbolizer = new LineSymbolizer();
 
-        final MockRule rule = new MockRule();
+        final Rule rule = new Rule();
         rule.symbolizers().add(symbolizer);
         rule.setFilter(filter);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
 
@@ -635,15 +640,15 @@ public class SEPortrayerTest extends TestCase {
                 filterFactory.literal("2"),
                 true, MatchAction.ANY);
 
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
-        symbolizer.perpendicularOffset = filterFactory.property("description", String.class);
+        final LineSymbolizer symbolizer = new LineSymbolizer();
+        symbolizer.setPerpendicularOffset((Expression)filterFactory.property("description", String.class));
 
-        final MockRule rule = new MockRule();
+        final Rule rule = new Rule();
         rule.symbolizers().add(symbolizer);
         rule.setFilter(filter);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
 
@@ -677,14 +682,14 @@ public class SEPortrayerTest extends TestCase {
      */
     @Test
     public void testGeometryExpression() {
-        final MockLineSymbolizer symbolizer = new MockLineSymbolizer();
-        symbolizer.geometry = filterFactory.function("ST_Centroid", filterFactory.property("geom"));
+        final LineSymbolizer symbolizer = new LineSymbolizer();
+        symbolizer.setGeometry(filterFactory.function("ST_Centroid", filterFactory.property("geom")));
 
-        final MockRule rule = new MockRule();
+        final Rule rule = new Rule();
         rule.symbolizers().add(symbolizer);
 
-        final MockStyle style = new MockStyle();
-        final MockFeatureTypeStyle fts = new MockFeatureTypeStyle();
+        final Style style = new Style();
+        final FeatureTypeStyle fts = new FeatureTypeStyle();
         style.featureTypeStyles().add(fts);
         fts.rules().add(rule);
 
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AbstractStyleTests.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AbstractStyleTests.java
new file mode 100644
index 0000000..0eea64c
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AbstractStyleTests.java
@@ -0,0 +1,28 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.util.SimpleInternationalString;
+import org.opengis.feature.Feature;
+import org.opengis.filter.Expression;
+import org.opengis.filter.FilterFactory;
+import org.opengis.util.InternationalString;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public abstract class AbstractStyleTests extends TestCase {
+
+    protected static final FilterFactory<Feature,Object,Object> FF = DefaultFilterFactory.forFeatures();
+    protected static final Expression<Feature,Double> EXP_DOUBLE = FF.literal(5.1);
+    protected static final Expression<Feature,Double> EXP_DOUBLE_2 = FF.literal(10.5);
+    protected static final Expression<Feature,String> EXP_STRING = FF.literal("some text");
+    protected static final Expression<Feature,Color> EXP_COLOR = FF.literal(Color.YELLOW);
+    protected static final InternationalString SAMPLE_ISTRING = new SimpleInternationalString("some international text");
+    protected static final String SAMPLE_STRING = "some text";
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java
new file mode 100644
index 0000000..8149c88
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/AnchorPointTest.java
@@ -0,0 +1,34 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.AnchorPoint}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class AnchorPointTest extends AbstractStyleTests {
+
+    /**
+     * Test of AnchorPointXY methods.
+     */
+    @Test
+    public void testAnchorPointXY() {
+        AnchorPoint cdt = new AnchorPoint();
+
+        //check defaults
+        assertEquals(FF.literal(0.5), cdt.getAnchorPointX());
+        assertEquals(FF.literal(0.5), cdt.getAnchorPointY());
+
+        //check get/set
+        cdt.setAnchorPointX(EXP_DOUBLE);
+        cdt.setAnchorPointY(EXP_DOUBLE_2);
+        assertEquals(EXP_DOUBLE, cdt.getAnchorPointX());
+        assertEquals(EXP_DOUBLE_2, cdt.getAnchorPointY());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java
new file mode 100644
index 0000000..f2567ca
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ChannelSelectionTest.java
@@ -0,0 +1,53 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ChannelSelection}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ChannelSelectionTest extends AbstractStyleTests {
+
+    /**
+     * Test of RGBChannels methods.
+     */
+    @Test
+    public void testRGBChannels() {
+        ChannelSelection cdt = new ChannelSelection();
+
+        //check default
+        assertArrayEquals(null, cdt.getRGBChannels());
+
+        //check get/set
+        SelectedChannelType[] value = new SelectedChannelType[]{
+            new SelectedChannelType("R", null),
+            new SelectedChannelType("G", null),
+            new SelectedChannelType("B", null)};
+        cdt.setRGBChannels(value);
+        assertArrayEquals(value, cdt.getRGBChannels());
+    }
+
+
+    /**
+     * Test of GrayChannel methods.
+     */
+    @Test
+    public void testGrayChannel() {
+        ChannelSelection cdt = new org.apache.sis.internal.style.ChannelSelection();
+
+        //check default
+        assertEquals(null, cdt.getGrayChannel());
+
+        //check get/set
+        cdt.setGray(new SelectedChannelType("Gr", null));
+        assertEquals(new SelectedChannelType("Gr", null), cdt.getGrayChannel());
+
+    }
+
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorMapTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorMapTest.java
new file mode 100644
index 0000000..71d884a
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorMapTest.java
@@ -0,0 +1,32 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ColorMap}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ColorMapTest extends AbstractStyleTests {
+
+    /**
+     * Test of Function methods.
+     */
+    @Test
+    public void testFunction() {
+        ColorMap cdt = new ColorMap();
+
+        //check default
+        assertEquals(null, cdt.getFunction());
+
+        //check get/set
+        cdt.setFunction(EXP_COLOR);
+        assertEquals(EXP_COLOR, cdt.getFunction());
+
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorReplacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorReplacementTest.java
new file mode 100644
index 0000000..d7cdea0
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ColorReplacementTest.java
@@ -0,0 +1,31 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ColorReplacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ColorReplacementTest extends AbstractStyleTests {
+
+    /**
+     * Test of Recoding methods.
+     */
+    @Test
+    public void testRecoding() {
+        ColorReplacement cdt = new ColorReplacement();
+
+        //check default
+        assertEquals(null, cdt.getRecoding());
+
+        //check get/set
+        cdt.setRecoding(EXP_COLOR);
+        assertEquals(EXP_COLOR, cdt.getRecoding());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java
new file mode 100644
index 0000000..531c57e
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ContrastEnhancementTest.java
@@ -0,0 +1,47 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.opengis.style.ContrastMethod;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ContrastEnhancement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ContrastEnhancementTest extends AbstractStyleTests {
+
+    /**
+     * Test of Method methods.
+     */
+    @Test
+    public void testMethod() {
+        ContrastEnhancement cdt = new ContrastEnhancement();
+
+        //check default
+        assertEquals(ContrastMethod.NONE, cdt.getMethod());
+
+        //check get/set
+        cdt.setMethod(ContrastMethod.HISTOGRAM);
+        assertEquals(ContrastMethod.HISTOGRAM, cdt.getMethod());
+    }
+
+    /**
+     * Test of GammaValue methods.
+     */
+    @Test
+    public void testGammaValue() {
+        ContrastEnhancement cdt = new ContrastEnhancement();
+
+        //check default
+        assertEquals(FF.literal(1.0), cdt.getGammaValue());
+
+        //check get/set
+        cdt.setGammaValue(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getGammaValue());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java
new file mode 100644
index 0000000..bc34dfc
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DescriptionTest.java
@@ -0,0 +1,38 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Description}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DescriptionTest extends AbstractStyleTests {
+
+    /**
+     * Test of Title methods.
+     */
+    @Test
+    public void testTitle() {
+        final Description cdt = new Description();
+        assertEquals(StyleFactory.EMPTY_STRING, cdt.getTitle());
+        cdt.setTitle(SAMPLE_ISTRING);
+        assertEquals(SAMPLE_ISTRING, cdt.getTitle());
+    }
+
+    /**
+     * Test of Abstract methods.
+     */
+    @Test
+    public void testAbstract() {
+        final Description cdt = new Description();
+        assertEquals(StyleFactory.EMPTY_STRING, cdt.getAbstract());
+        cdt.setAbstract(SAMPLE_ISTRING);
+        assertEquals(SAMPLE_ISTRING, cdt.getAbstract());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java
new file mode 100644
index 0000000..d801f6c
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/DisplacementTest.java
@@ -0,0 +1,34 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Displacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class DisplacementTest extends AbstractStyleTests {
+
+    /**
+     * Test of DisplacementXY methods.
+     */
+    @Test
+    public void testGetDisplacementXY() {
+        Displacement cdt = new Displacement();
+
+        //check defaults
+        assertEquals(FF.literal(0.0), cdt.getDisplacementX());
+        assertEquals(FF.literal(0.0), cdt.getDisplacementY());
+
+        //check get/set
+        cdt.setDisplacementX(EXP_DOUBLE);
+        cdt.setDisplacementY(EXP_DOUBLE_2);
+        assertEquals(EXP_DOUBLE, cdt.getDisplacementX());
+        assertEquals(EXP_DOUBLE_2, cdt.getDisplacementY());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java
new file mode 100644
index 0000000..48a139f
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalGraphicTest.java
@@ -0,0 +1,82 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.Arrays;
+import java.util.Collection;
+import javax.swing.ImageIcon;
+import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ExternalGraphic}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ExternalGraphicTest extends AbstractStyleTests {
+
+    /**
+     * Test of OnlineResource methods.
+     */
+    @Test
+    public void testOnlineResource() {
+        ExternalGraphic cdt = new ExternalGraphic();
+
+        //check defaults
+        assertEquals(null, cdt.getOnlineResource());
+
+        //check get/set
+        cdt.setOnlineResource(new DefaultOnlineResource());
+        assertEquals(new DefaultOnlineResource(), cdt.getOnlineResource());
+    }
+
+    /**
+     * Test of InlineContent methods.
+     */
+    @Test
+    public void testInlineContent() {
+        ExternalGraphic cdt = new ExternalGraphic();
+
+        //check defaults
+        assertEquals(null, cdt.getInlineContent());
+
+        //check get/set
+        ImageIcon value = new ImageIcon();
+        cdt.setInlineContent(value);
+        assertEquals(value, cdt.getInlineContent());
+    }
+
+    /**
+     * Test of Format methods.
+     */
+    @Test
+    public void testFormat() {
+        ExternalGraphic cdt = new ExternalGraphic();
+
+        //check defaults
+        assertEquals(null, cdt.getFormat());
+
+        //check get/set
+        cdt.setFormat(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getFormat());
+    }
+
+    /**
+     * Test of ColorReplacements methods.
+     */
+    @Test
+    public void testColorReplacements() {
+        ExternalGraphic cdt = new ExternalGraphic();
+
+        //check defaults
+        assertEquals(null, cdt.getColorReplacements());
+
+        //check get/set
+        Collection<ColorReplacement> value = Arrays.asList(new ColorReplacement());
+        cdt.setColorReplacements(value);
+        assertEquals(value, cdt.getColorReplacements());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalMarkTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalMarkTest.java
new file mode 100644
index 0000000..d6a9d1e
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ExternalMarkTest.java
@@ -0,0 +1,80 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import javax.swing.ImageIcon;
+import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ExternalMark}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ExternalMarkTest extends AbstractStyleTests {
+
+    /**
+     * Test of OnlineResource methods.
+     */
+    @Test
+    public void testOnlineResource() {
+        ExternalMark cdt = new ExternalMark();
+
+        //check defaults
+        assertEquals(null, cdt.getOnlineResource());
+
+        //check get/set
+        cdt.setOnlineResource(new DefaultOnlineResource());
+        assertEquals(new DefaultOnlineResource(), cdt.getOnlineResource());
+    }
+
+    /**
+     * Test of InlineContent methods.
+     */
+    @Test
+    public void testInlineContent() {
+        ExternalMark cdt = new ExternalMark();
+
+        //check defaults
+        assertEquals(null, cdt.getInlineContent());
+
+        //check get/set
+        ImageIcon value = new ImageIcon();
+        cdt.setInlineContent(value);
+        assertEquals(value, cdt.getInlineContent());
+    }
+
+    /**
+     * Test of Format methods.
+     */
+    @Test
+    public void testFormat() {
+        ExternalMark cdt = new ExternalMark();
+
+        //check defaults
+        assertEquals(null, cdt.getFormat());
+
+        //check get/set
+        cdt.setFormat(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getFormat());
+    }
+
+    /**
+     * Test of MarkIndex methods.
+     */
+    @Test
+    public void testMarkIndex() {
+        ExternalMark cdt = new ExternalMark();
+
+        //check defaults
+        assertEquals(0, cdt.getMarkIndex());
+
+        //check get/set
+        cdt.setMarkIndex(10);
+        assertEquals(10, cdt.getMarkIndex());
+    }
+
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java
new file mode 100644
index 0000000..16fa78e
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FeatureTypeStyleTest.java
@@ -0,0 +1,143 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.util.List;
+import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+import org.apache.sis.util.iso.Names;
+import org.junit.Test;
+import static org.junit.Assert.*;
+import org.opengis.filter.InvalidFilterValueException;
+import org.opengis.filter.ResourceId;
+import org.opengis.style.SemanticType;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.FeatureTypeStyle}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class FeatureTypeStyleTest extends AbstractStyleTests {
+
+    /**
+     * Test of Name methods.
+     */
+    @Test
+    public void testName() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertEquals(null, cdt.getName());
+
+        //check get/set
+        cdt.setName(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getName());
+    }
+
+    /**
+     * Test of Description methods.
+     */
+    @Test
+    public void testDescription() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertEquals(null, cdt.getDescription());
+
+        //check get/set
+        cdt.setDescription(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING));
+        assertEquals(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING), cdt.getDescription());
+    }
+
+    /**
+     * Test of FeatureInstanceIDs methods.
+     */
+    @Test
+    public void testFeatureInstanceIDs() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertEquals(null, cdt.getFeatureInstanceIDs());
+
+        //check get/set
+        final ResourceId rid = new ResourceId() {
+            @Override
+            public String getIdentifier() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public List getExpressions() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public boolean test(Object object) throws InvalidFilterValueException {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+        };
+        cdt.setFeatureInstanceIDs(rid);
+        assertEquals(rid, cdt.getFeatureInstanceIDs());
+    }
+
+    /**
+     * Test of featureTypeNames methods.
+     */
+    @Test
+    public void testFeatureTypeNames() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertTrue(cdt.featureTypeNames().isEmpty());
+
+        //check get/set
+        cdt.featureTypeNames().add(Names.createLocalName(null, null, "test"));
+        assertEquals(1, cdt.featureTypeNames().size());
+    }
+
+    /**
+     * Test of semanticTypeIdentifiers methods.
+     */
+    @Test
+    public void testSemanticTypeIdentifiers() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertTrue(cdt.semanticTypeIdentifiers().isEmpty());
+
+        //check get/set
+        cdt.semanticTypeIdentifiers().add(SemanticType.LINE);
+        assertEquals(1, cdt.semanticTypeIdentifiers().size());
+    }
+
+    /**
+     * Test of rules methods.
+     */
+    @Test
+    public void testRules() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertTrue(cdt.rules().isEmpty());
+
+        //check get/set
+        cdt.rules().add(new Rule());
+        assertEquals(1, cdt.rules().size());
+    }
+
+    /**
+     * Test of OnlineResource methods.
+     */
+    @Test
+    public void testOnlineResource() {
+        FeatureTypeStyle cdt = new FeatureTypeStyle();
+
+        //check defaults
+        assertEquals(null, cdt.getOnlineResource());
+
+        //check get/set
+        cdt.setOnlineResource(new DefaultOnlineResource());
+        assertEquals(new DefaultOnlineResource(), cdt.getOnlineResource());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java
new file mode 100644
index 0000000..5cb4afa
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FillTest.java
@@ -0,0 +1,63 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Fill}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class FillTest extends AbstractStyleTests {
+
+    /**
+     * Test of GraphicFill methods.
+     */
+    @Test
+    public void testGraphicFill() {
+        Fill cdt = new Fill();
+
+        //check default
+        assertEquals(null, cdt.getGraphicFill());
+
+        //check get/set
+        final GraphicFill value = new GraphicFill();
+        cdt.setGraphicFill(value);
+        assertEquals(value, cdt.getGraphicFill());
+    }
+
+    /**
+     * Test of Color methods.
+     */
+    @Test
+    public void testColor() {
+        Fill cdt = new Fill();
+
+        //check default
+        assertEquals(FF.literal(Color.GRAY), cdt.getColor());
+
+        //check get/set
+        cdt.setColor(EXP_COLOR);
+        assertEquals(EXP_COLOR, cdt.getColor());
+    }
+
+    /**
+     * Test of Opacity methods.
+     */
+    @Test
+    public void testOpacity() {
+        Fill cdt = new Fill();
+
+        //check default
+        assertEquals(FF.literal(1.0), cdt.getOpacity());
+
+        //check get/set
+        cdt.setOpacity(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getOpacity());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java
new file mode 100644
index 0000000..53f420d
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/FontTest.java
@@ -0,0 +1,74 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+/**
+ * Tests for {@link org.apache.sis.internal.style.Font}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class FontTest extends AbstractStyleTests {
+
+    /**
+     * Test of Family methods.
+     */
+    @Test
+    public void testFamily() {
+        Font cdt = new Font();
+
+        //check default
+        assertTrue(cdt.getFamily().isEmpty());
+
+        //check get/set
+        cdt.getFamily().add(EXP_STRING);
+        assertEquals(1, cdt.getFamily().size());
+    }
+    /**
+     * Test of Style methods.
+     */
+    @Test
+    public void testStyle() {
+        Font cdt = new Font();
+
+        //check default
+        assertEquals(FF.literal("normal"), cdt.getStyle());
+
+        //check get/set
+        cdt.setStyle(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getStyle());
+    }
+
+    /**
+     * Test of Weight methods.
+     */
+    @Test
+    public void testWeight() {
+        Font cdt = new Font();
+
+        //check default
+        assertEquals(FF.literal("normal"), cdt.getWeight());
+
+        //check get/set
+        cdt.setWeight(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getWeight());
+    }
+
+    /**
+     * Test of Size methods.
+     */
+    @Test
+    public void testSize() {
+        Font cdt = new Font();
+
+        //check default
+        assertEquals(FF.literal(10.0), cdt.getSize());
+
+        //check get/set
+        cdt.setSize(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getSize());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java
new file mode 100644
index 0000000..449272d
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicStrokeTest.java
@@ -0,0 +1,46 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.GraphicStroke}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class GraphicStrokeTest extends AbstractStyleTests {
+
+    /**
+     * Test of InitialGap methods.
+     */
+    @Test
+    public void testInitialGap() {
+        GraphicStroke cdt = new GraphicStroke();
+
+        //check default
+        assertEquals(null, cdt.getInitialGap());
+
+        //check get/set
+        cdt.setInitialGap(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getInitialGap());
+    }
+
+    /**
+     * Test of Gap methods.
+     */
+    @Test
+    public void testGap() {
+        GraphicStroke cdt = new GraphicStroke();
+
+        //check default
+        assertEquals(null, cdt.getGap());
+
+        //check get/set
+        cdt.setGap(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getGap());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java
new file mode 100644
index 0000000..133d186
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/GraphicTest.java
@@ -0,0 +1,108 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Graphic}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class GraphicTest extends AbstractStyleTests {
+
+    /**
+     * Test of graphicalSymbols methods.
+     */
+    @Test
+    public void testGraphicalSymbols() {
+        Graphic cdt = new Graphic();
+
+        //check defaults
+        assertTrue(cdt.graphicalSymbols().isEmpty());
+
+        //check get/set
+        cdt.graphicalSymbols().add(new Mark());
+        assertEquals(1, cdt.graphicalSymbols().size());
+    }
+
+    /**
+     * Test of Opacity methods.
+     */
+    @Test
+    public void testOpacity() {
+        Graphic cdt = new Graphic();
+
+        //check default
+        assertEquals(null, cdt.getOpacity());
+
+        //check get/set
+        cdt.setOpacity(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getOpacity());
+    }
+
+    /**
+     * Test of Size methods.
+     */
+    @Test
+    public void testSize() {
+        Graphic cdt = new Graphic();
+
+        //check default
+        assertEquals(null, cdt.getSize());
+
+        //check get/set
+        cdt.setSize(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getSize());
+    }
+
+    /**
+     * Test of Rotation methods.
+     */
+    @Test
+    public void testRotation() {
+        Graphic cdt = new Graphic();
+
+        //check default
+        assertEquals(null, cdt.getRotation());
+
+        //check get/set
+        cdt.setRotation(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getRotation());
+    }
+
+    /**
+     * Test of AnchorPoint methods.
+     */
+    @Test
+    public void testAnchorPoint() {
+        Graphic cdt = new Graphic();
+
+        //check default
+        assertEquals(null, cdt.getAnchorPoint());
+
+        //check get/set
+        AnchorPoint value = new AnchorPoint(EXP_DOUBLE, EXP_DOUBLE_2);
+        cdt.setAnchorPoint(value);
+        assertEquals(value, cdt.getAnchorPoint());
+    }
+
+    /**
+     * Test of Displacement methods.
+     */
+    @Test
+    public void testDisplacement() {
+        Graphic cdt = new Graphic();
+
+        //check default
+        assertEquals(null, cdt.getDisplacement());
+
+        //check get/set
+        Displacement value = new Displacement(EXP_DOUBLE, EXP_DOUBLE_2);
+        cdt.setDisplacement(value);
+        assertEquals(value, cdt.getDisplacement());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java
new file mode 100644
index 0000000..2cd2843
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/HaloTest.java
@@ -0,0 +1,47 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import static org.apache.sis.internal.style.StyleFactory.*;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Halo}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class HaloTest extends AbstractStyleTests {
+
+    /**
+     * Test of Fill methods.
+     */
+    @Test
+    public void testFill() {
+        Halo cdt = new Halo();
+
+        //check default
+        assertEquals(new Fill(null, LITERAL_WHITE, DEFAULT_FILL_OPACITY), cdt.getFill());
+
+        //check get/set
+        cdt.setFill(new Fill(null, EXP_COLOR, EXP_DOUBLE));
+        assertEquals(new Fill(null, EXP_COLOR, EXP_DOUBLE), cdt.getFill());
+    }
+
+    /**
+     * Test of Radius methods.
+     */
+    @Test
+    public void testRadius() {
+        Halo cdt = new Halo();
+
+        //check default
+        assertEquals(FF.literal(1.0), cdt.getRadius());
+
+        //check get/set
+        cdt.setRadius(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getRadius());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java
new file mode 100644
index 0000000..1c2f1ae
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LinePlacementTest.java
@@ -0,0 +1,106 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.LinePlacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class LinePlacementTest extends AbstractStyleTests {
+
+    /**
+     * Test of PerpendicularOffset methods.
+     */
+    @Test
+    public void testPerpendicularOffset() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertEquals(null, cdt.getPerpendicularOffset());
+
+        //check get/set
+        cdt.setPerpendicularOffset(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getPerpendicularOffset());
+    }
+
+    /**
+     * Test of InitialGap methods.
+     */
+    @Test
+    public void testInitialGap() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertEquals(null, cdt.getInitialGap());
+
+        //check get/set
+        cdt.setInitialGap(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getInitialGap());
+    }
+
+    /**
+     * Test of Gap methods.
+     */
+    @Test
+    public void testGap() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertEquals(null, cdt.getGap());
+
+        //check get/set
+        cdt.setGap(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getGap());
+    }
+
+    /**
+     * Test of isRepeated methods.
+     */
+    @Test
+    public void testIsRepeated() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertFalse(cdt.isRepeated());
+
+        //check get/set
+        cdt.setRepeated(true);
+        assertTrue(cdt.isRepeated());
+    }
+
+    /**
+     * Test of IsAligned methods.
+     */
+    @Test
+    public void testIsAligned() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertFalse(cdt.IsAligned());
+
+        //check get/set
+        cdt.setAligned(true);
+        assertTrue(cdt.IsAligned());
+    }
+
+    /**
+     * Test of isGeneralizeLine methods.
+     */
+    @Test
+    public void testIsGeneralizeLine() {
+        LinePlacement cdt = new LinePlacement();
+
+        //check default
+        assertFalse(cdt.isGeneralizeLine());
+
+        //check get/set
+        cdt.setGeneralizeLine(true);
+        assertTrue(cdt.isGeneralizeLine());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java
new file mode 100644
index 0000000..e08a61d
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/LineSymbolizerTest.java
@@ -0,0 +1,47 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.LineSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class LineSymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of Stroke methods.
+     */
+    @Test
+    public void testStroke() {
+        LineSymbolizer cdt = new LineSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getStroke());
+
+        //check get/set
+        Stroke value = new Stroke();
+        cdt.setStroke(value);
+        assertEquals(value, cdt.getStroke());
+    }
+
+    /**
+     * Test of PerpendicularOffset methods.
+     */
+    @Test
+    public void testPerpendicularOffset() {
+        LineSymbolizer cdt = new LineSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getPerpendicularOffset());
+
+        //check get/set
+        cdt.setPerpendicularOffset(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getPerpendicularOffset());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java
new file mode 100644
index 0000000..944feb9
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/MarkTest.java
@@ -0,0 +1,78 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Mark}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class MarkTest extends AbstractStyleTests {
+
+    /**
+     * Test of WellKnownName methods.
+     */
+    @Test
+    public void testWellKnownName() {
+        Mark cdt = new Mark();
+
+        //check default
+        assertEquals(FF.literal("square"), cdt.getWellKnownName());
+
+        //check get/set
+        cdt.setWellKnownName(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getWellKnownName());
+    }
+
+    /**
+     * Test of ExternalMark methods.
+     */
+    @Test
+    public void testExternalMark() {
+        Mark cdt = new Mark();
+
+        //check default
+        assertEquals(null, cdt.getExternalMark());
+
+        //check get/set
+        ExternalMark value = new ExternalMark();
+        cdt.setExternalMark(value);
+        assertEquals(value, cdt.getExternalMark());
+    }
+
+    /**
+     * Test of Fill methods.
+     */
+    @Test
+    public void testFill() {
+        Mark cdt = new Mark();
+
+        //check default
+        assertEquals(new Fill(), cdt.getFill());
+
+        //check get/set
+        cdt.setFill(new Fill(null, EXP_COLOR, EXP_DOUBLE));
+        assertEquals(new Fill(null, EXP_COLOR, EXP_DOUBLE), cdt.getFill());
+    }
+
+    /**
+     * Test of getStroke methods.
+     */
+    @Test
+    public void testStroke() {
+        Mark cdt = new Mark();
+
+        //check default
+        assertEquals(new Stroke(), cdt.getStroke());
+
+        //check get/set
+        Stroke value = new Stroke();
+        cdt.setStroke(value);
+        assertEquals(value, cdt.getStroke());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java
new file mode 100644
index 0000000..659c45e
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointPlacementTest.java
@@ -0,0 +1,63 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.PointPlacement}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class PointPlacementTest extends AbstractStyleTests {
+
+    /**
+     * Test of AnchorPoint methods.
+     */
+    @Test
+    public void testAnchorPoint() {
+        PointPlacement cdt = new PointPlacement();
+
+        //check default
+        assertEquals(null, cdt.getAnchorPoint());
+
+        //check get/set
+        AnchorPoint value = new AnchorPoint(EXP_DOUBLE, EXP_DOUBLE_2);
+        cdt.setAnchorPoint(value);
+        assertEquals(value, cdt.getAnchorPoint());
+    }
+
+    /**
+     * Test of Displacement methods.
+     */
+    @Test
+    public void testDisplacement() {
+        PointPlacement cdt = new PointPlacement();
+
+        //check default
+        assertEquals(null, cdt.getDisplacement());
+
+        //check get/set
+        Displacement value = new Displacement(EXP_DOUBLE, EXP_DOUBLE_2);
+        cdt.setDisplacement(value);
+        assertEquals(value, cdt.getDisplacement());
+    }
+
+    /**
+     * Test of Rotation methods.
+     */
+    @Test
+    public void testRotation() {
+        PointPlacement cdt = new PointPlacement();
+
+        //check default
+        assertEquals(null, cdt.getRotation());
+
+        //check get/set
+        cdt.setRotation(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getRotation());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java
new file mode 100644
index 0000000..abb23c8
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PointSymbolizerTest.java
@@ -0,0 +1,32 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.PointSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class PointSymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of Graphic methods.
+     */
+    @Test
+    public void testGraphic() {
+        PointSymbolizer cdt = new PointSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getGraphic());
+
+        //check get/set
+        Graphic value = new Graphic();
+        cdt.setGraphic(value);
+        assertEquals(value, cdt.getGraphic());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java
new file mode 100644
index 0000000..3e76e7b
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/PolygonSymbolizerTest.java
@@ -0,0 +1,79 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.PolygonSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class PolygonSymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of Stroke methods.
+     */
+    @Test
+    public void testStroke() {
+        PolygonSymbolizer cdt = new PolygonSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getStroke());
+
+        //check get/set
+        Stroke value = new Stroke();
+        cdt.setStroke(value);
+        assertEquals(value, cdt.getStroke());
+    }
+
+    /**
+     * Test of Fill methods.
+     */
+    @Test
+    public void testFill() {
+        PolygonSymbolizer cdt = new PolygonSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getFill());
+
+        //check get/set
+        Fill value = new Fill();
+        cdt.setFill(value);
+        assertEquals(value, cdt.getFill());
+    }
+
+    /**
+     * Test of Displacement methods.
+     */
+    @Test
+    public void testDisplacement() {
+        PolygonSymbolizer cdt = new PolygonSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getDisplacement());
+
+        //check get/set
+        Displacement value = new Displacement(EXP_DOUBLE, EXP_DOUBLE_2);
+        cdt.setDisplacement(value);
+        assertEquals(value, cdt.getDisplacement());
+    }
+
+    /**
+     * Test of PerpendicularOffset methods.
+     */
+    @Test
+    public void testPerpendicularOffset() {
+        PolygonSymbolizer cdt = new PolygonSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getPerpendicularOffset());
+
+        //check get/set
+        cdt.setPerpendicularOffset(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getPerpendicularOffset());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java
new file mode 100644
index 0000000..e49e7be
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RasterSymbolizerTest.java
@@ -0,0 +1,127 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.opengis.style.OverlapBehavior;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.RasterSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class RasterSymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of Opacity methods.
+     */
+    @Test
+    public void testOpacity() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getOpacity());
+
+        //check get/set
+        cdt.setOpacity(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getOpacity());
+    }
+
+    /**
+     * Test of ChannelSelection methods.
+     */
+    @Test
+    public void testChannelSelection() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getChannelSelection());
+
+        //check get/set
+        ChannelSelection value = new ChannelSelection();
+        cdt.setChannelSelection(value);
+        assertEquals(value, cdt.getChannelSelection());
+    }
+
+    /**
+     * Test of OverlapBehavior methods.
+     */
+    @Test
+    public void testOverlapBehavior() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getOverlapBehavior());
+
+        //check get/set
+        cdt.setOverlapBehavior(OverlapBehavior.EARLIEST_ON_TOP);
+        assertEquals(OverlapBehavior.EARLIEST_ON_TOP, cdt.getOverlapBehavior());
+    }
+
+    /**
+     * Test of ColorMap methods.
+     */
+    @Test
+    public void testColorMap() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getColorMap());
+
+        //check get/set
+        ColorMap value = new ColorMap();
+        cdt.setColorMap(value);
+        assertEquals(value, cdt.getColorMap());
+    }
+
+    /**
+     * Test of ContrastEnhancement methods.
+     */
+    @Test
+    public void testGetContrastEnhancement() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getContrastEnhancement());
+
+        //check get/set
+        ContrastEnhancement value = new ContrastEnhancement();
+        cdt.setContrastEnhancement(value);
+        assertEquals(value, cdt.getContrastEnhancement());
+    }
+
+    /**
+     * Test of ShadedRelief methods.
+     */
+    @Test
+    public void testGetShadedRelief() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getShadedRelief());
+
+        //check get/set
+        ShadedRelief value = new ShadedRelief();
+        cdt.setShadedRelief(value);
+        assertEquals(value, cdt.getShadedRelief());
+    }
+
+    /**
+     * Test of ImageOutline methods.
+     */
+    @Test
+    public void testImageOutline() {
+        RasterSymbolizer cdt = new RasterSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getImageOutline());
+
+        //check get/set
+        Symbolizer value = new LineSymbolizer();
+        cdt.setImageOutline(value);
+        assertEquals(value, cdt.getImageOutline());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java
new file mode 100644
index 0000000..83900f4
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/RuleTest.java
@@ -0,0 +1,155 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.opengis.filter.Filter;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Rule}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class RuleTest extends AbstractStyleTests {
+
+    /**
+     * Test of Name methods.
+     */
+    @Test
+    public void testGetName() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(null, cdt.getName());
+
+        //check get/set
+        cdt.setName(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getName());
+    }
+
+    /**
+     * Test of Description methods.
+     */
+    @Test
+    public void testDescription() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(null, cdt.getDescription());
+
+        //check get/set
+        cdt.setDescription(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING));
+        assertEquals(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING), cdt.getDescription());
+    }
+
+    /**
+     * Test of Legend methods.
+     */
+    @Test
+    public void testLegend() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(null, cdt.getLegend());
+
+        //check get/set
+        GraphicLegend value = new GraphicLegend();
+        cdt.setLegend(value);
+        assertEquals(value, cdt.getLegend());
+    }
+
+    /**
+     * Test of Filter methods.
+     */
+    @Test
+    public void testFilter() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(null, cdt.getFilter());
+
+        //check get/set
+        Filter value = FF.equal(EXP_STRING, EXP_STRING);
+        cdt.setFilter(value);
+        assertEquals(value, cdt.getFilter());
+    }
+
+    /**
+     * Test of ElseFilter methods.
+     */
+    @Test
+    public void testIsElseFilter() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertFalse(cdt.isElseFilter());
+
+        //check get/set
+        cdt.setElseFilter(true);
+        assertTrue(cdt.isElseFilter());
+    }
+
+    /**
+     * Test of MinScaleDenominator methods.
+     */
+    @Test
+    public void testMinScaleDenominator() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(0.0, cdt.getMinScaleDenominator(), 0.0);
+
+        //check get/set
+        cdt.setMinScaleDenominator(10.0);
+        assertEquals(10.0, cdt.getMinScaleDenominator(), 0.0);
+    }
+
+    /**
+     * Test of MaxScaleDenominator methods.
+     */
+    @Test
+    public void testGetMaxScaleDenominator() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(Double.MAX_VALUE, cdt.getMaxScaleDenominator(), 0.0);
+
+        //check get/set
+        cdt.setMaxScaleDenominator(10.0);
+        assertEquals(10.0, cdt.getMaxScaleDenominator(), 0.0);
+    }
+
+    /**
+     * Test of symbolizers methods.
+     */
+    @Test
+    public void testSymbolizers() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertTrue(cdt.symbolizers().isEmpty());
+
+        //check get/set
+        cdt.symbolizers().add(new LineSymbolizer());
+        assertEquals(1, cdt.symbolizers().size());
+    }
+
+    /**
+     * Test of OnlineResource methods.
+     */
+    @Test
+    public void testGetOnlineResource() {
+        Rule cdt = new Rule();
+
+        //check defaults
+        assertEquals(null, cdt.getOnlineResource());
+
+        //check get/set
+        cdt.setOnlineResource(new DefaultOnlineResource());
+        assertEquals(new DefaultOnlineResource(), cdt.getOnlineResource());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java
new file mode 100644
index 0000000..7fb8945
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SelectedChannelTypeTest.java
@@ -0,0 +1,47 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.SelectedChannelType}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class SelectedChannelTypeTest extends AbstractStyleTests {
+
+    /**
+     * Test of ChannelName methods.
+     */
+    @Test
+    public void testChannelName() {
+        SelectedChannelType cdt = new SelectedChannelType();
+
+        //check defaults
+        assertEquals(null, cdt.getChannelName());
+
+        //check get/set
+        cdt.setChannelName(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getChannelName());
+    }
+
+    /**
+     * Test of ContrastEnhancement methods.
+     */
+    @Test
+    public void testContrastEnhancement() {
+        SelectedChannelType cdt = new SelectedChannelType();
+
+        //check defaults
+        assertEquals(null, cdt.getContrastEnhancement());
+
+        //check get/set
+        ContrastEnhancement value = new ContrastEnhancement();
+        cdt.setContrastEnhancement(value);
+        assertEquals(value, cdt.getContrastEnhancement());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java
new file mode 100644
index 0000000..4b49976
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/ShadedReliefTest.java
@@ -0,0 +1,46 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.ShadedRelief}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class ShadedReliefTest extends AbstractStyleTests {
+
+    /**
+     * Test of isBrightnessOnly methods.
+     */
+    @Test
+    public void testIsBrightnessOnly() {
+        ShadedRelief cdt = new ShadedRelief();
+
+        //check default
+        assertFalse(cdt.isBrightnessOnly());
+
+        //check get/set
+        cdt.setBrightnessOnly(true);
+        assertTrue(cdt.isBrightnessOnly());
+    }
+
+    /**
+     * Test of ReliefFactor methods.
+     */
+    @Test
+    public void testReliefFactor() {
+        ShadedRelief cdt = new ShadedRelief();
+
+        //check default
+        assertEquals(null, cdt.getReliefFactor());
+
+        //check get/set
+        cdt.setReliefFactor(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getReliefFactor());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java
new file mode 100644
index 0000000..bc2565b
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StrokeTest.java
@@ -0,0 +1,154 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import java.awt.Color;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Stroke}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class StrokeTest extends AbstractStyleTests {
+
+    /**
+     * Test of GraphicFill methods.
+     */
+    @Test
+    public void testGraphicFill() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(null, cdt.getGraphicFill());
+
+        //check get/set
+        GraphicFill value = new GraphicFill();
+        cdt.setGraphicFill(value);
+        assertEquals(value, cdt.getGraphicFill());
+    }
+
+    /**
+     * Test of GraphicStroke methods.
+     */
+    @Test
+    public void testGraphicStroke() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(null, cdt.getGraphicStroke());
+
+        //check get/set
+        GraphicStroke value = new GraphicStroke();
+        cdt.setGraphicStroke(value);
+        assertEquals(value, cdt.getGraphicStroke());
+    }
+
+    /**
+     * Test of Color methods.
+     */
+    @Test
+    public void testColor() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal(Color.BLACK), cdt.getColor());
+
+        //check get/set
+        cdt.setColor(EXP_COLOR);
+        assertEquals(EXP_COLOR, cdt.getColor());
+    }
+
+    /**
+     * Test of Opacity methods.
+     */
+    @Test
+    public void testOpacity() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal(1.0), cdt.getOpacity());
+
+        //check get/set
+        cdt.setOpacity(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getOpacity());
+    }
+
+    /**
+     * Test of Width methods.
+     */
+    @Test
+    public void testWidth() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal(1.0), cdt.getWidth());
+
+        //check get/set
+        cdt.setWidth(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getWidth());
+    }
+
+    /**
+     * Test of LineJoin methods.
+     */
+    @Test
+    public void testLineJoin() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal("bevel"), cdt.getLineJoin());
+
+        //check get/set
+        cdt.setLineJoin(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getLineJoin());
+    }
+
+    /**
+     * Test of LineCap methods.
+     */
+    @Test
+    public void testLineCap() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal("square"), cdt.getLineCap());
+
+        //check get/set
+        cdt.setLineCap(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getLineCap());
+    }
+
+    /**
+     * Test of DashArray methods.
+     */
+    @Test
+    public void testDashArray() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(null, cdt.getDashArray());
+
+        //check get/set
+        cdt.setDashArray(new float[]{1,2,3});
+        assertArrayEquals(new float[]{1,2,3}, cdt.getDashArray(), 0.0f);
+    }
+
+    /**
+     * Test of DashOffset methods.
+     */
+    @Test
+    public void testDashOffset() {
+        Stroke cdt = new Stroke();
+
+        //check default
+        assertEquals(FF.literal(0.0), cdt.getDashOffset());
+
+        //check get/set
+        cdt.setDashOffset(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getDashOffset());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java
new file mode 100644
index 0000000..65257ea
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/StyleTest.java
@@ -0,0 +1,92 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Style}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class StyleTest extends AbstractStyleTests {
+
+    /**
+     * Test of featureTypeStyles methods.
+     */
+    @Test
+    public void testFeatureTypeStyles() {
+        Style cdt = new Style();
+
+        //check defaults
+        assertTrue(cdt.featureTypeStyles().isEmpty());
+
+        //check get/set
+        cdt.featureTypeStyles().add(new FeatureTypeStyle());
+        assertEquals(1, cdt.featureTypeStyles().size());
+    }
+
+    /**
+     * Test of isDefault methods.
+     */
+    @Test
+    public void testIsDefault() {
+        Style cdt = new Style();
+
+        //check defaults
+        assertFalse(cdt.isDefault());
+
+        //check get/set
+        cdt.setDefault(true);
+        assertTrue(cdt.isDefault());
+    }
+
+    /**
+     * Test of DefaultSpecification methods.
+     */
+    @Test
+    public void testDefaultSpecification() {
+        Style cdt = new Style();
+
+        //check defaults
+        assertEquals(null, cdt.getDefaultSpecification());
+
+        //check get/set
+        Symbolizer value = new LineSymbolizer();
+        cdt.setDefaultSpecification(value);
+        assertEquals(value, cdt.getDefaultSpecification());
+    }
+
+    /**
+     * Test of getName methods.
+     */
+    @Test
+    public void testName() {
+        Style cdt = new Style();
+
+        //check defaults
+        assertEquals(null, cdt.getName());
+
+        //check get/set
+        cdt.setName(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getName());
+    }
+
+    /**
+     * Test of Description methods.
+     */
+    @Test
+    public void testDescription() {
+        Style cdt = new Style();
+
+        //check defaults
+        assertEquals(null, cdt.getDescription());
+
+        //check get/set
+        cdt.setDescription(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING));
+        assertEquals(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING), cdt.getDescription());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java
new file mode 100644
index 0000000..f36fe11
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/SymbolizerTest.java
@@ -0,0 +1,77 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.apache.sis.measure.Units;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.Symbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class SymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of UnitOfMeasure methods.
+     */
+    @Test
+    public void testUnitOfMeasure() {
+        Symbolizer cdt = new LineSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getUnitOfMeasure());
+
+        //check get/set
+        cdt.setUnitOfMeasure(Units.INCH);
+        assertEquals(Units.INCH, cdt.getUnitOfMeasure());
+    }
+
+    /**
+     * Test of Geometry methods.
+     */
+    @Test
+    public void testGeometry() {
+        Symbolizer cdt = new LineSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getGeometry());
+
+        //check get/set
+        cdt.setGeometry(EXP_DOUBLE);
+        assertEquals(EXP_DOUBLE, cdt.getGeometry());
+    }
+
+    /**
+     * Test of Name methods.
+     */
+    @Test
+    public void testName() {
+        Symbolizer cdt = new LineSymbolizer();
+
+        //check defaults
+        assertEquals(null, cdt.getName());
+
+        //check get/set
+        cdt.setName(SAMPLE_STRING);
+        assertEquals(SAMPLE_STRING, cdt.getName());
+    }
+
+    /**
+     * Test of getDescription methods.
+     */
+    @Test
+    public void testDescription() {
+        Symbolizer cdt = new LineSymbolizer();
+
+        //check defaults
+        assertEquals(null, cdt.getDescription());
+
+        //check get/set
+        cdt.setDescription(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING));
+        assertEquals(new Description(SAMPLE_ISTRING, SAMPLE_ISTRING), cdt.getDescription());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java
new file mode 100644
index 0000000..55e9537
--- /dev/null
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/internal/style/TextSymbolizerTest.java
@@ -0,0 +1,95 @@
+/*
+ * (C) 2022, Geomatys
+ */
+package org.apache.sis.internal.style;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link org.apache.sis.internal.style.TextSymbolizer}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class TextSymbolizerTest extends AbstractStyleTests {
+
+    /**
+     * Test of Label methods.
+     */
+    @Test
+    public void testLabel() {
+        TextSymbolizer cdt = new TextSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getLabel());
+
+        //check get/set
+        cdt.setLabel(EXP_STRING);
+        assertEquals(EXP_STRING, cdt.getLabel());
+    }
+
+    /**
+     * Test of Font methods.
+     */
+    @Test
+    public void testFont() {
+        TextSymbolizer cdt = new TextSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getFont());
+
+        //check get/set
+        Font value = new Font();
+        cdt.setFont(value);
+        assertEquals(value, cdt.getFont());
+    }
+
+    /**
+     * Test of LabelPlacement methods.
+     */
+    @Test
+    public void testLabelPlacement() {
+        TextSymbolizer cdt = new TextSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getLabelPlacement());
+
+        //check get/set
+        LabelPlacement value = new PointPlacement();
+        cdt.setLabelPlacement(value);
+        assertEquals(value, cdt.getLabelPlacement());
+    }
+
+    /**
+     * Test of Halo methods.
+     */
+    @Test
+    public void testHalo() {
+        TextSymbolizer cdt = new TextSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getHalo());
+
+        //check get/set
+        Halo value = new Halo();
+        cdt.setHalo(value);
+        assertEquals(value, cdt.getHalo());
+    }
+
+    /**
+     * Test of Fill methods.
+     */
+    @Test
+    public void testFill() {
+        TextSymbolizer cdt = new TextSymbolizer();
+
+        //check default
+        assertEquals(null, cdt.getFill());
+
+        //check get/set
+        Fill value = new Fill();
+        cdt.setFill(value);
+        assertEquals(value, cdt.getFill());
+    }
+
+}
diff --git a/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java b/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
index 8824566..85cb3da 100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/test/suite/PortrayalTestSuite.java
@@ -32,7 +32,36 @@ import org.junit.runners.Suite;
 @Suite.SuiteClasses({
     org.apache.sis.portrayal.MapLayersTest.class,
     org.apache.sis.internal.map.SEPortrayerTest.class,
-    org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoaderTest.class
+    org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoaderTest.class,
+    org.apache.sis.internal.style.AnchorPointTest.class,
+    org.apache.sis.internal.style.ChannelSelectionTest.class,
+    org.apache.sis.internal.style.ColorMapTest.class,
+    org.apache.sis.internal.style.ColorReplacementTest.class,
+    org.apache.sis.internal.style.ContrastEnhancementTest.class,
+    org.apache.sis.internal.style.DescriptionTest.class,
+    org.apache.sis.internal.style.DisplacementTest.class,
+    org.apache.sis.internal.style.ExternalGraphicTest.class,
+    org.apache.sis.internal.style.ExternalMarkTest.class,
+    org.apache.sis.internal.style.FeatureTypeStyleTest.class,
+    org.apache.sis.internal.style.FillTest.class,
+    org.apache.sis.internal.style.FontTest.class,
+    org.apache.sis.internal.style.GraphicStrokeTest.class,
+    org.apache.sis.internal.style.GraphicTest.class,
+    org.apache.sis.internal.style.HaloTest.class,
+    org.apache.sis.internal.style.LinePlacementTest.class,
+    org.apache.sis.internal.style.LineSymbolizerTest.class,
+    org.apache.sis.internal.style.MarkTest.class,
+    org.apache.sis.internal.style.PointPlacementTest.class,
+    org.apache.sis.internal.style.PointSymbolizerTest.class,
+    org.apache.sis.internal.style.PolygonSymbolizerTest.class,
+    org.apache.sis.internal.style.RasterSymbolizerTest.class,
+    org.apache.sis.internal.style.RuleTest.class,
+    org.apache.sis.internal.style.SelectedChannelTypeTest.class,
+    org.apache.sis.internal.style.ShadedReliefTest.class,
+    org.apache.sis.internal.style.StrokeTest.class,
+    org.apache.sis.internal.style.StyleTest.class,
+    org.apache.sis.internal.style.SymbolizerTest.class,
+    org.apache.sis.internal.style.TextSymbolizerTest.class
 })
 public final strictfp class PortrayalTestSuite extends TestSuite {
     /**