You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/07/01 15:23:08 UTC

[sis] 01/01: Merge the style classes from branch 'geoapi-3.1'.

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

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 2f048bc7a0f49e618fafcf9487446d4a28e7298c
Merge: d59f279d23 27cab0a6de
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jul 1 15:14:40 2023 +0200

    Merge the style classes from branch 'geoapi-3.1'.
    
    https://issues.apache.org/jira/browse/SIS-583

 .../org/apache/sis/feature/FeatureOperations.java  |   2 +-
 .../main/java/org/apache/sis/xml/Namespaces.java   |  18 +-
 core/sis-portrayal/pom.xml                         |   5 +
 .../src/main/java/org/apache/sis/style/Style.java  |  29 ++
 .../java/org/apache/sis/style/package-info.java    |  41 ++
 .../org/apache/sis/style/se1/AbstractStyle.java    | 348 ++++++++++++++
 .../java/org/apache/sis/style/se1/AnchorPoint.java | 160 ++++++
 .../org/apache/sis/style/se1/ChannelSelection.java | 193 ++++++++
 .../java/org/apache/sis/style/se1/ColorMap.java    |  94 ++++
 .../org/apache/sis/style/se1/ColorReplacement.java |  87 ++++
 .../apache/sis/style/se1/ContrastEnhancement.java  | 168 +++++++
 .../org/apache/sis/style/se1/ContrastMethod.java   |  54 +++
 .../org/apache/sis/style/se1/CoverageStyle.java    |  68 +++
 .../java/org/apache/sis/style/se1/Description.java | 160 ++++++
 .../org/apache/sis/style/se1/Displacement.java     | 163 +++++++
 .../java/org/apache/sis/style/se1/ElseFilter.java  |  51 ++
 .../apache/sis/style/se1/ExpressionAdapter.java    |  52 ++
 .../org/apache/sis/style/se1/ExternalGraphic.java  | 139 ++++++
 .../org/apache/sis/style/se1/FeatureTypeStyle.java |  81 ++++
 .../main/java/org/apache/sis/style/se1/Fill.java   | 248 ++++++++++
 .../main/java/org/apache/sis/style/se1/Font.java   | 224 +++++++++
 .../java/org/apache/sis/style/se1/Graphic.java     | 351 ++++++++++++++
 .../java/org/apache/sis/style/se1/GraphicFill.java | 138 ++++++
 .../org/apache/sis/style/se1/GraphicStroke.java    | 203 ++++++++
 .../org/apache/sis/style/se1/GraphicalElement.java |  52 ++
 .../org/apache/sis/style/se1/GraphicalSymbol.java  | 181 +++++++
 .../main/java/org/apache/sis/style/se1/Halo.java   | 170 +++++++
 .../org/apache/sis/style/se1/LabelPlacement.java   |  80 +++
 .../org/apache/sis/style/se1/LegendGraphic.java    | 130 +++++
 .../org/apache/sis/style/se1/LinePlacement.java    | 283 +++++++++++
 .../org/apache/sis/style/se1/LineSymbolizer.java   | 173 +++++++
 .../main/java/org/apache/sis/style/se1/Mark.java   | 293 +++++++++++
 .../org/apache/sis/style/se1/OverlapBehavior.java  |  52 ++
 .../org/apache/sis/style/se1/PointPlacement.java   | 209 ++++++++
 .../org/apache/sis/style/se1/PointSymbolizer.java  | 172 +++++++
 .../apache/sis/style/se1/PolygonSymbolizer.java    | 324 +++++++++++++
 .../org/apache/sis/style/se1/RasterSymbolizer.java | 358 ++++++++++++++
 .../main/java/org/apache/sis/style/se1/Rule.java   | 513 ++++++++++++++++++++
 .../org/apache/sis/style/se1/SelectedChannel.java  | 173 +++++++
 .../org/apache/sis/style/se1/SemanticType.java     |  67 +++
 .../org/apache/sis/style/se1/ShadedRelief.java     | 155 ++++++
 .../main/java/org/apache/sis/style/se1/Stroke.java | 492 +++++++++++++++++++
 .../org/apache/sis/style/se1/StyleElement.java     | 219 +++++++++
 .../org/apache/sis/style/se1/StyleFactory.java     | 534 +++++++++++++++++++++
 .../java/org/apache/sis/style/se1/Symbolizer.java  | 314 ++++++++++++
 .../java/org/apache/sis/style/se1/Symbology.java   | 255 ++++++++++
 .../org/apache/sis/style/se1/TextSymbolizer.java   | 288 +++++++++++
 .../java/org/apache/sis/style/se1/Translucent.java |  49 ++
 .../org/apache/sis/style/se1/package-info.java     |  66 +++
 .../org/apache/sis/style/se1/AnchorPointTest.java  |  53 ++
 .../apache/sis/style/se1/ChannelSelectionTest.java |  71 +++
 .../sis/style/se1/ContrastEnhancementTest.java     |  67 +++
 .../org/apache/sis/style/se1/DescriptionTest.java  |  60 +++
 .../org/apache/sis/style/se1/DisplacementTest.java |  53 ++
 .../apache/sis/style/se1/ExternalGraphicTest.java  | 102 ++++
 .../apache/sis/style/se1/FeatureTypeStyleTest.java | 134 ++++++
 .../java/org/apache/sis/style/se1/FillTest.java    |  93 ++++
 .../java/org/apache/sis/style/se1/FontTest.java    | 100 ++++
 .../apache/sis/style/se1/GraphicStrokeTest.java    |  65 +++
 .../java/org/apache/sis/style/se1/GraphicTest.java | 128 +++++
 .../java/org/apache/sis/style/se1/HaloTest.java    |  73 +++
 .../apache/sis/style/se1/LinePlacementTest.java    | 125 +++++
 .../apache/sis/style/se1/LineSymbolizerTest.java   |  72 +++
 .../java/org/apache/sis/style/se1/MarkTest.java    |  86 ++++
 .../apache/sis/style/se1/PointPlacementTest.java   |  90 ++++
 .../apache/sis/style/se1/PointSymbolizerTest.java  |  57 +++
 .../sis/style/se1/PolygonSymbolizerTest.java       | 109 +++++
 .../apache/sis/style/se1/RasterSymbolizerTest.java | 146 ++++++
 .../java/org/apache/sis/style/se1/RuleTest.java    | 184 +++++++
 .../apache/sis/style/se1/SelectedChannelTest.java  |  68 +++
 .../org/apache/sis/style/se1/ShadedReliefTest.java |  66 +++
 .../java/org/apache/sis/style/se1/StrokeTest.java  | 187 ++++++++
 .../org/apache/sis/style/se1/StyleTestCase.java    | 120 +++++
 .../org/apache/sis/style/se1/SymbolizerTest.java   | 103 ++++
 .../org/apache/sis/style/se1/SymbologyTest.java    | 114 +++++
 .../apache/sis/style/se1/TextSymbolizerTest.java   | 122 +++++
 .../java/org/apache/sis/style/se1/XmlTest.java     |  50 ++
 77 files changed, 11375 insertions(+), 2 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index 3e79790812,7986a36733..4324ac0310
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
@@@ -314,7 -307,7 +314,7 @@@ public final class FeatureOperations ex
      /**
       * Creates an operation which delegates the computation to a given expression.
       * The {@code expression} argument should generally be an instance of
--     * {@link org.opengis.filter.Expression},
++     * {@link org.apache.sis.filter.Expression},
       * but more generic functions are accepted as well.
       *
       * <h4>Read/write behavior</h4>
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
index 0000000000,0bcc54eada..8b0dc11e3b
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AbstractStyle.java
@@@ -1,0 -1,347 +1,348 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Set;
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.EnumSet;
+ import java.util.Optional;
+ import jakarta.xml.bind.Unmarshaller;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlAttribute;
+ import org.opengis.util.GenericName;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.ResourceId;
++import org.apache.sis.filter.Filter;
+ 
+ 
+ /**
+  * Defines the styling that is to be applied on data of some arbitrary type.
+  * The type of data is specified by the {@code <R>} parameterized type and depends on the concrete subclass.
 - * The two main types are {@link org.opengis.feature.Feature} and {@link org.apache.sis.coverage.BandedCoverage}.
++ * The two main types are {@link org.apache.sis.feature.AbstractFeature}
++ * and {@link org.apache.sis.coverage.BandedCoverage}.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since   1.5
+  */
+ @XmlType(propOrder = {
+     "name",
+     "description",
+     "featureTypeName",
+ //  "semanticTypeIdentifiers",
+     "rules"
+ })
+ public abstract class AbstractStyle<R> extends StyleElement<R> {
+     /**
+      * Version number of the Symbology Encoding Implementation Specification standard currently implemented.
+      * This version number may change in future Apache SIS releases if new standards are published.
+      * The current value is {@value}.
+      */
+     public static final String VERSION = "1.1.0";
+ 
+     /**
+      * Version number of the Symbology Encoding standard used.
+      * This value should not be changed, unless this style has been unmarshalled
+      * from a XML document that specifies a different version number.
+      *
+      * @see #getVersion()
+      */
+     @XmlAttribute
+     protected String version;
+ 
+     /**
+      * Name for this style, or {@code null} if none.
+      *
+      * @see #getName()
+      * @see #setName(String)
+      */
+     @XmlElement(name = "Name")
+     protected String name;
+ 
+     /**
+      * Information for user interfaces, or {@code null} if none.
+      *
+      * @see #getDescription()
+      * @see #setDescription(Description)
+      */
+     @XmlElement(name = "Description")
+     protected Description<R> description;
+ 
+     /**
+      * Identification of feature instances on which to apply the style, or {@code null} if none.
+      *
+      * @see #getFeatureInstanceIDs()
+      * @see #setFeatureInstanceIDs(ResourceId)
+      */
 -    protected ResourceId<? super R> featureInstanceIDs;
++    protected Filter<? super R> featureInstanceIDs;
+ 
+     /**
+      * Name of the feature type that this style is meant to act upon, or {@code null} if none.
+      *
+      * @see #getFeatureTypeName()
+      * @see #setFeatureTypeName(GenericName)
+      */
+     @XmlElement(name = "FeatureTypeName")
+     protected GenericName featureTypeName;
+ 
+     /**
+      * Types of geometry that this style is meant to act upon, as a live collection.
+      * May be empty but never null.
+      *
+      * @see #semanticTypeIdentifiers()
+      */
+ //  @XmlElement(name = "SemanticTypeIdentifier")
+     private EnumSet<SemanticType> semanticTypeIdentifiers;
+ 
+     /**
+      * List of rules.
+      *
+      * <h4>Online rules</h4>
+      * A XML document may contain links to rules instead of a full definitions.
+      * Such {@code se:OnlineResource} elements are handled at marshalling time.
+      * When reading a XML document, the rule links are resolved automatically.
+      * When writing a XML document, some rules may be replaced by online resources
+      * if {@link Rule#getOnlineSource()} is provided.
+      *
+      * @todo JAXB adapter for handling the online case is not yet written.
+      *
+      * @see #rules()
+      */
+     @XmlElement(name = "Rule")
+     private List<Rule<R>> rules;
+ 
+     /**
+      * Invoked by JAXB before unmarshalling this mark.
+      * Avoid giving the false impression that the XML document contained a version string.
+      */
+     private void beforeUnmarshal(Unmarshaller caller, Object parent) {
+         version = "unspecified";
+     }
+ 
+     /**
+      * Creates an initially empty feature type style.
+      * Callers should set the following properties after construction:
+      *
+      * <ul>
+      *   <li>Either the {@linkplain #setFeatureTypeName feature type name}
+      *       or {@linkplain #semanticTypeIdentifiers() semantic type identifiers}, or both.</li>
+      *   <li>At least one {@linkplain #rules() rule} should be added.</li>
+      * </ul>
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public AbstractStyle(final StyleFactory<R> factory) {
+         super(factory);
+         version = VERSION;
+         semanticTypeIdentifiers = EnumSet.noneOf(SemanticType.class);
+         rules = new ArrayList<>();
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public AbstractStyle(final AbstractStyle<R> source) {
+         super(source);
+         version                 = source.version;
+         name                    = source.name;
+         description             = source.description;
+         featureInstanceIDs      = source.featureInstanceIDs;
+         featureTypeName         = source.featureTypeName;
+         semanticTypeIdentifiers = source.semanticTypeIdentifiers.clone();
+         rules                   = new ArrayList<>(source.rules);
+     }
+ 
+     /**
+      * Returns the version number of the Symbology Encoding standard used.
+      * This is fixed to {@value #VERSION} in current Apache SIS release.
+      * This value cannot be changed, unless this style has been unmarshalled
+      * from a XML document that specifies a different version number.
+      *
+      * @return version number of the Symbology Encoding standard used.
+      */
+     public String getVersion() {
+         return version;
+     }
+ 
+     /**
+      * Returns the name for this style.
+      * This can be any string that uniquely identifies this style within a given canvas.
+      * It is not meant to be human-friendly. For a human-friendly label,
+      * see the {@linkplain Description#getTitle() title} instead.
+      *
+      * @return a name for this style.
+      */
+     public Optional<String> getName() {
+         return Optional.ofNullable(name);
+     }
+ 
+     /**
+      * Sets a name for this style.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new name for this style.
+      */
+     public void setName(final String value) {
+         name = value;
+     }
+ 
+     /**
+      * Returns the description of this style.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this style, and conversely.
+      *
+      * @return information for user interfaces.
+      */
+     public Optional<Description<R>> getDescription() {
+         return Optional.ofNullable(description);
+     }
+ 
+     /**
+      * Sets a description of this style.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new information for user interfaces, or {@code null} if none.
+      */
+     public void setDescription(final Description<R> value) {
+         description = value;
+     }
+ 
+     /**
+      * Returns an identification of feature instances on which to apply the style.
+      * This method enable the possibility to use a feature type style on a given list
+      * of features only, instead of all instances of the feature type.
+      *
+      * @return identification of the feature instances.
+      */
 -    public Optional<ResourceId<? super R>> getFeatureInstanceIDs() {
++    public Optional<Filter<? super R>> getFeatureInstanceIDs() {
+         return Optional.ofNullable(featureInstanceIDs);
+     }
+ 
+     /**
+      * Sets an identification of feature instances on which to apply the style.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new identification of feature instances, or {@code null} if none.
+      */
 -    public void setFeatureInstanceIDs(final ResourceId<? super R> value) {
++    public void setFeatureInstanceIDs(final Filter<? super R> value) {
+         featureInstanceIDs = value;
+     }
+ 
+     /**
+      * Returns the name of the feature type that this style is meant to act upon.
+      * It is allowed to be null but only if the feature type can be inferred by other means,
+      * for example from context or using {@link SemanticType} identifiers.
+      *
+      * @return name of the feature type that this style is meant to act upon.
+      */
+     public Optional<GenericName> getFeatureTypeName() {
+         return Optional.ofNullable(featureTypeName);
+     }
+ 
+     /**
+      * Sets the name of the feature type that this style is meant to act upon.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new name of the feature type, or {@code null} if none.
+      */
+     public void setFeatureTypeName(final GenericName value) {
+         featureTypeName = value;
+     }
+ 
+     /**
+      * Returns the most general types of geometry that this style is meant to act upon.
+      * The syntax is currently undefined, but the following values are reserved to indicate
+      * that the style applies to feature with default geometry of specific type:
+      *
+      * <ul>
+      *   <li>{@code generic:point}</li>
+      *   <li>{@code generic:line}</li>
+      *   <li>{@code generic:polygon}</li>
+      *   <li>{@code generic:text}</li>
+      *   <li>{@code generic:raster}</li>
+      *   <li>{@code generic:any}</li>
+      * </ul>
+      *
+      * <p>The returned collection is <em>live</em>:
+      * changes in that collection are reflected into this object, and conversely.</p>
+      *
+      * @return types of geometry that this style is meant to act upon, as a live collection.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public Set<SemanticType> semanticTypeIdentifiers() {
+         return semanticTypeIdentifiers;
+     }
+ 
+     /**
+      * Returns the list of rules contained by this style.
+      * Order matter: the first item in a list will be the
+      * first item plotted and hence appears on the bottom.
+      *
+      * <p>The returned collection is <em>live</em>:
+      * changes in that collection are reflected into this object, and conversely.</p>
+      *
+      * @return ordered list of rules, as a live collection.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public List<Rule<R>> rules() {
+         return rules;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {name, description, featureInstanceIDs, featureTypeName, semanticTypeIdentifiers, rules};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public AbstractStyle<R> clone() {
+         final var clone = (AbstractStyle<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (description != null) description = description.clone();
+         semanticTypeIdentifiers = semanticTypeIdentifiers.clone();
+         rules = new ArrayList<>(rules);
+         rules.replaceAll(Rule::clone);
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
index 0000000000,bd2c32907f..dc9cdf95e6
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/AnchorPoint.java
@@@ -1,0 -1,160 +1,160 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * The location inside a graphic or label to use as an "anchor" for positioning it relative to a point.
+  * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers between 0 and 1 inclusive.
+  * The bounding box of the graphic/label to be rendered is considered to be in a coordinate space from 0
+  * (lower-left corner) to 1 (upper-right corner), and the anchor position is specified as a point in this space.
+  * The default anchor point is <var>x</var> = 0.5, <var>y</var> = 0.5,
+  * which is at the middle height and middle length of the graphic/label.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Ian Turton (CCG)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "AnchorPointType", propOrder = {
+     "anchorPointX",
+     "anchorPointY"
+ })
+ @XmlRootElement(name = "AnchorPoint")
+ public class AnchorPoint<R> extends StyleElement<R> {
+     /**
+      * The <var>x</var> coordinate of the anchor point.
+      * This property is mandatory.
+      *
+      * @see #getAnchorPointX()
+      * @see #setAnchorPointX(Expression)
+      */
+     @XmlElement(name = "AnchorPointX", required = true)
+     protected Expression<R, ? extends Number> anchorPointX;
+ 
+     /**
+      * The <var>y</var> coordinate of the anchor point.
+      * This property is mandatory.
+      *
+      * @see #getAnchorPointY()
+      * @see #setAnchorPointY(Expression)
+      */
+     @XmlElement(name = "AnchorPointY", required = true)
+     protected Expression<R, ? extends Number> anchorPointY;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private AnchorPoint() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
+      * This initial position is the center of the graphic/label.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public AnchorPoint(final StyleFactory<R> factory) {
+         super(factory);
+         anchorPointX = anchorPointY = factory.half;
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public AnchorPoint(final AnchorPoint<R> source) {
+         super(source);
+         anchorPointX = source.anchorPointX;
+         anchorPointY = source.anchorPointY;
+     }
+ 
+     /**
+      * Returns the <var>x</var> coordinate of the anchor point.
+      * It should be a floating point number between 0 and 1 inclusive.
+      *
+      * @return the expression fetching the <var>x</var> coordinate.
+      */
+     public Expression<R, ? extends Number> getAnchorPointX() {
+         return anchorPointX;
+     }
+ 
+     /**
+      * Sets the <var>x</var> coordinate of the anchor point.
+      * If this method is never invoked, then the default value is literal 0.5.
+      *
+      * @param  value  new <var>x</var> coordinate, or {@code null} for resetting the default value.
+      */
+     public void setAnchorPointX(final Expression<R, ? extends Number> value) {
+         anchorPointX = defaultToHalf(value);
+     }
+ 
+     /**
+      * Returns the <var>y</var> coordinate of the anchor point.
+      * It should be a floating point number between 0 and 1 inclusive.
+      *
+      * @return the expression fetching the <var>y</var> coordinate.
+      */
+     public Expression<R, ? extends Number> getAnchorPointY() {
+         return anchorPointY;
+     }
+ 
+     /**
+      * Sets the <var>y</var> coordinate of the anchor point.
+      * If this method is never invoked, then the default value is literal 0.5.
+      *
+      * @param  value  new <var>y</var> coordinate, or {@code null} for resetting the default value.
+      */
+     public void setAnchorPointY(final Expression<R, ? extends Number> value) {
+         anchorPointY = defaultToHalf(value);
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {anchorPointX, anchorPointY};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public AnchorPoint<R> clone() {
+         return (AnchorPoint<R>) super.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
index 0000000000,2069d30b05..3540992206
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ContrastEnhancement.java
@@@ -1,0 -1,168 +1,168 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Contrast enhancement for an image or an individual image channel.
+  * In the case of a color image, the relative grayscale brightness of a pixel color is used.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Ian Turton (CCG)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "ContrastEnhancementType", propOrder = {
+ //  "normalize",
+ //  "histogram",
+     "gammaValue"
+ })
+ @XmlRootElement(name = "ContrastEnhancement")
+ public class ContrastEnhancement<R> extends StyleElement<R> {
+     /**
+      * Method to use for applying contrast enhancement, or {@code null} for the default value.
+      * The default value depends on whether or not a {@linkplain #gammaValue gamma value} is defined.
+      *
+      * @see #getMethod()
+      * @see #setMethod(ContrastMethod)
+      *
+      * @todo Marshall as empty "Normalize" or "Histogram" XML element.
+      */
+     protected ContrastMethod method;
+ 
+     /**
+      * How much to brighten or dim an image, or {@code null} for the default value.
+      *
+      * @see #getGammaValue()
+      * @see #setGammaValue(Expression)
+      *
+      * @todo Add a JAXB adapter for marshalling as a plain number.
+      */
+     @XmlElement(name = "GammaValue")
+     protected Expression<R, ? extends Number> gammaValue;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private ContrastEnhancement() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a contrast enhancement initialized to no operation.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public ContrastEnhancement(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public ContrastEnhancement(final ContrastEnhancement<R> source) {
+         super(source);
+         method     = source.method;
+         gammaValue = source.gammaValue;
+     }
+ 
+     /**
+      * Returns the method to use for applying contrast enhancement.
+      *
+      * @return method to use for applying contrast enhancement.
+      */
+     public ContrastMethod getMethod() {
+         final var value = method;
+         if (value != null) {
+             return value;
+         }
+         return (gammaValue != null) ? ContrastMethod.GAMMA : ContrastMethod.NONE;
+     }
+ 
+     /**
+      * Sets the method to use for applying contrast enhancement.
+      * Setting this method to anything else than {@link ContrastMethod#GAMMA}
+      * clears the {@linkplain #getGammaValue() gamma value}.
+      *
+      * @param  value  new method to use, or {@code null} for none.
+      */
+     public void setMethod(final ContrastMethod value) {
+         method = value;
+         if (value != ContrastMethod.GAMMA) {
+             gammaValue = null;
+         }
+     }
+ 
+     /**
+      * Tells how much to brighten (values greater than 1) or dim (values less than 1) an image.
+      * A value of 1 means no change.
+      *
+      * @return expression to control gamma adjustment.
+      */
+     public Expression<R, ? extends Number> getGammaValue() {
+         return defaultToOne(gammaValue);
+     }
+ 
+     /**
+      * Sets how much to brighten (values greater than 1) or dim (values less than 1) an image.
+      * Setting a non-null value sets the method to {@link ContrastMethod#GAMMA}.
+      * If this method is never invoked, then the default value is literal 1.
+      *
+      * @param  value  new expression to control gamma adjustment, or {@code null} for the default.
+      */
+     public void setGammaValue(final Expression<R, ? extends Number> value) {
+         gammaValue = value;
+         if (value != null) {
+             method = null;
+         }
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {method, gammaValue};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public ContrastEnhancement<R> clone() {
+         return (ContrastEnhancement<R>) super.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
index 0000000000,84702112c9..b9d8fc311e
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Displacement.java
@@@ -1,0 -1,163 +1,163 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * A two-dimensional offset from the original geometry.
+  * Displacements may be used to avoid over-plotting, or for supplying shadows.
+  * The displacements units depend on the context:
+  * in {@linkplain Symbolizer#getUnitOfMeasure() symbolizer unit of measurements}
+  * when the displacement is applied by a {@link PolygonSymbolizer},
+  * but in pixels when the displacement is applied by a {@link TextSymbolizer}.
+  * The displacements are to the right of the point.
+  * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Ian Turton (CCG)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  *
+  * @see PointPlacement#getDisplacement()
+  * @see Graphic#getDisplacement()
+  * @see PolygonSymbolizer#getDisplacement()
+  */
+ @XmlType(name = "DisplacementType", propOrder = {
+     "displacementX",
+     "displacementY"
+ })
+ @XmlRootElement(name = "Displacement")
+ public class Displacement<R> extends StyleElement<R> {
+     /**
+      * The <var>x</var> offset from the geometry point.
+      * This property is mandatory.
+      *
+      * @see #getDisplacementX()
+      * @see #setDisplacementX(Expression)
+      */
+     @XmlElement(name = "DisplacementX", required = true)
+     protected Expression<R, ? extends Number> displacementX;
+ 
+     /**
+      * The <var>y</var> offset from the geometry point.
+      * This property is mandatory.
+      *
+      * @see #getDisplacementY()
+      * @see #setDisplacementY(Expression)
+      */
+     @XmlElement(name = "DisplacementY", required = true)
+     protected Expression<R, ? extends Number> displacementY;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Displacement() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a displacement initialized to zero offsets.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Displacement(final StyleFactory<R> factory) {
+         super(factory);
+         displacementX = displacementY = factory.zero;
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Displacement(final Displacement<R> source) {
+         super(source);
+         displacementX = source.displacementX;
+         displacementY = source.displacementY;
+     }
+ 
+     /**
+      * Returns an expression fetching an <var>x</var> offset from the geometry point.
+      *
+      * @return <var>x</var> offset from the geometry point.
+      */
+     public Expression<R, ? extends Number> getDisplacementX() {
+         return displacementX;
+     }
+ 
+     /**
+      * Sets the expression fetching an <var>x</var> offset.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new <var>x</var> offset, or {@code null} for resetting the default value.
+      */
+     public void setDisplacementX(final Expression<R, ? extends Number> value) {
+         displacementX = defaultToZero(value);
+     }
+ 
+     /**
+      * Returns an expression fetching an <var>y</var> offset from the geometry point.
+      *
+      * @return <var>y</var> offset from the geometry point.
+      */
+     public Expression<R, ? extends Number> getDisplacementY() {
+         return displacementY;
+     }
+ 
+     /**
+      * Sets the expression fetching an <var>y</var> offset.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new <var>y</var> offset, or {@code null} for resetting the default value.
+      */
+     public void setDisplacementY(final Expression<R, ? extends Number> value) {
+         displacementY = defaultToZero(value);
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {displacementX, displacementY};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Displacement<R> clone() {
+         return (Displacement<R>) super.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
index 0000000000,3ea09310de..a5adcb5d2a
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ExpressionAdapter.java
@@@ -1,0 -1,52 +1,52 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.adapters.XmlAdapter;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Adapter for expression in style.
+  * This is a place-holder for future work.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ final class ExpressionAdapter<R> extends XmlAdapter<String, Expression<R,?>> {
+     /**
+      * Creates an adapter.
+      */
+     public ExpressionAdapter() {
+     }
+ 
+     @Override
+     public String marshal(Expression<R,?> value) {
+         return null;
+     }
+ 
+     @Override
+     public Expression<R,?> unmarshal(String value) {
+         return null;
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
index 0000000000,6aa50648ee..73b192bac6
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/FeatureTypeStyle.java
@@@ -1,0 -1,81 +1,81 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ import org.apache.sis.filter.DefaultFilterFactory;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Defines the styling that is to be applied to a single feature type.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ @XmlType(name = "FeatureTypeStyleType")
+ @XmlRootElement(name = "FeatureTypeStyle")
 -public class FeatureTypeStyle extends AbstractStyle<Feature> {
++public class FeatureTypeStyle extends AbstractStyle<AbstractFeature> {
+     /**
+      * The default style factory for features.
+      */
 -    public static final StyleFactory<Feature> FACTORY =
++    public static final StyleFactory<AbstractFeature> FACTORY =
+             new StyleFactory<>(DefaultFilterFactory.forFeatures());
+ 
+     /**
+      * Creates an initially empty feature type style.
+      * Callers should set the following properties after construction:
+      *
+      * <ul>
+      *   <li>Either the {@linkplain #setFeatureTypeName feature type name}
+      *       or {@linkplain #semanticTypeIdentifiers() semantic type identifiers}, or both.</li>
+      *   <li>At least one {@linkplain #rules() rule} should be added.</li>
+      * </ul>
+      */
+     public FeatureTypeStyle() {
+         super(FACTORY);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public FeatureTypeStyle(final FeatureTypeStyle source) {
+         super(source);
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public FeatureTypeStyle clone() {
+         final var clone = (FeatureTypeStyle) super.clone();
+         return clone;
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
index 0000000000,0f8717a441..dc1d63f54c
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Fill.java
@@@ -1,0 -1,248 +1,248 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to fill the interior of polygons.
+  * A fill can be a solid color or a repeated {@link GraphicFill}.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "FillType", propOrder = {
+     "graphicFill",
+ //  "svgParameter"
+ })
+ @XmlRootElement(name = "Fill")
+ public class Fill<R> extends StyleElement<R> implements Translucent<R> {
+     /**
+      * The image to use for filling the area, or {@code null} if a solid color should be used.
+      *
+      * @see #getGraphicFill()
+      * @see #setGraphicFill(GraphicFill)
+      */
+     @XmlElement(name = "GraphicFill")
+     protected GraphicFill<R> graphicFill;
+ 
+     /**
+      * Color of the interior if it is to be solid-color filled, or {@code null} for the default value.
+      * The default value specified by OGC 05-077r4 standard is gray.
+      *
+      * <p>This property is used when {@link #graphicFill} is null.
+      * In XML documents, this is encoded inside a {@code <SvgParameter name="fill">} element.</p>
+      *
+      * @see #getColor()
+      * @see #setColor(Expression)
+      */
+     protected Expression<R,Color> color;
+ 
+     /**
+      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+      * The default value specified by OGC 05-077r4 standard is 1.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="fill-opacity">} element.</p>
+      *
+      * @see #getOpacity()
+      * @see #setOpacity(Expression)
+      */
+     protected Expression<R, ? extends Number> opacity;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Fill() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an opaque fill initialized to the gray color.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Fill(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Fill(final Fill<R> source) {
+         super(source);
+         graphicFill = source.graphicFill;
+         color       = source.color;
+         opacity     = source.opacity;
+     }
+ 
+     /**
+      * If filling with tiled copies of an image, returns the image that should be drawn.
+      * Otherwise returns an empty value, which means that a solid color should be used.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this fill, and conversely.</p>
+      *
+      * @return image to repeat for filling the area.
+      *
+      * @see Stroke#getGraphicFill()
+      */
+     public Optional<GraphicFill<R>> getGraphicFill() {
+         return Optional.ofNullable(graphicFill);
+     }
+ 
+     /**
+      * Specifies that area should be filled with the given graphic.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new image to repeat for filling the area, or {@code null} if none.
+      *
+      * @see Stroke#setGraphicFill(GraphicFill)
+      */
+     public void setGraphicFill(final GraphicFill<R> value) {
+         graphicFill = value;
+     }
+ 
+     /**
+      * Indicates the color of the area if it is to be solid-color filled.
+      * This is used when {@linkplain #getGraphicFill() graphic fill} is null.
+      *
+      * @return color of the area if it is to be solid-color filled.
+      *
+      * @see Stroke#getColor()
+      */
+     public Expression<R,Color> getColor() {
+         final var value = color;
+         return (value != null) ? value : factory.gray;
+     }
+ 
+     /**
+      * Sets the color of the area if it is to be solid-color filled.
+      * If this method is never invoked, then the default value is {@link Fill#GRAY}.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * <p>Setting a non-null value clears the {@linkplain #getGraphicFill() graphic fill}
+      * because those two properties are mutually exclusive.</p>
+      *
+      * @param  value  color of the area if solid-color filled, or {@code null} for resetting the default value.
+      *
+      * @see Stroke#setColor(Expression)
+      */
+     public void setColor(final Expression<R,Color> value) {
+         color = value;
+         if (value != null) {
+             graphicFill = null;
+         }
+     }
+ 
+     /**
+      * Sets the color and opacity together.
+      * The opacity is derived from the alpha value of the given color.
+      *
+      * @param  value  new color and opacity, or {@code null} for resetting the defaults.
+      */
+     public void setColorAndOpacity(Color value) {
+         if (value  == null) {
+             color   = null;
+             opacity = null;
+         } else {
+             if ((opacity = opacity(value)) != null) {
+                 value = new Color(value.getRGB() | 0xFF000000);
+             }
+             color = literal(value);
+         }
+     }
+ 
+     /**
+      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+      *
+      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+      *
+      * @see Stroke#getOpacity()
+      * @see Graphic#getOpacity()
+      * @see RasterSymbolizer#getOpacity()
+      */
+     @Override
+     public Expression<R, ? extends Number> getOpacity() {
+         return defaultToOne(opacity);
+     }
+ 
+     /**
+      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * If this method is never invoked, then the default value is literal 1 (totally opaque).
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new level of translucency, or {@code null} for resetting the default value.
+      */
+     @Override
+     public void setOpacity(final Expression<R, ? extends Number> value) {
+         opacity = value;
+     }
+ 
+     /*
+      * TODO: we need a private method like below for formatting above SVG parameters:
+      * See Stroke for more detais.
+      */
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {graphicFill, color, opacity};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Fill<R> clone() {
+         final var clone = (Fill<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (graphicFill != null) graphicFill = graphicFill.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
index 0000000000,ea91e8d00c..a32b7822cb
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Font.java
@@@ -1,0 -1,224 +1,224 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.List;
+ import java.util.ArrayList;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Identification of a font of a certain family, style, and size.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "FontType")
+ @XmlRootElement(name = "Font")
+ public class Font<R> extends StyleElement<R> {
+     /**
+      * Family names of the font to use, in preference order.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-family">} element.</p>
+      *
+      * @see #family()
+      */
+     private List<Expression<R,String>> family;
+ 
+     /**
+      * Style (normal or italic) to use for a font.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-style">} element.</p>
+      *
+      * @see #getStyle()
+      * @see #setStyle(Expression)
+      */
+     protected Expression<R,String> style;
+ 
+     /**
+      * Amount of weight or boldness to use for a font.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-weight">} element.</p>
+      *
+      * @see #getWeight()
+      * @see #setWeight(Expression)
+      */
+     protected Expression<R,String> weight;
+ 
+     /**
+      * Size (in pixels) to use for the font.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="font-size">} element.</p>
+      *
+      * @see #getSize()
+      * @see #setSize(Expression)
+      */
+     protected Expression<R, ? extends Number> size;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Font() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a font initialized to normal style, normal weight and a size of 10 pixels.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Font(final StyleFactory<R> factory) {
+         super(factory);
+         family = new ArrayList<>();
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Font(final Font<R> source) {
+         super(source);
+         family = new ArrayList<>(source.family);
+         style  = source.style;
+         weight = source.weight;
+         size   = source.size;
+     }
+ 
+     /**
+      * Returns the family names of the font to use, in preference order.
+      * Allowed values are system-dependent.
+      *
+      * <p>The returned collection is <em>live</em>:
+      * changes in that collection are reflected into this object, and conversely.</p>
+      *
+      * @return the family names in preference order, as a live collection.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public List<Expression<R,String>> family() {
+         return family;
+     }
+ 
+     /**
+      * Returns the style (normal or italic) to use for a font.
+      * The allowed values are "normal", "italic", and "oblique".
+      *
+      * @return style to use for a font.
+      */
+     public Expression<R,String> getStyle() {
+         final var value = style;
+         return (value != null) ? value : factory.normal;
+     }
+ 
+     /**
+      * Sets the style (normal or italic) to use for a font.
+      * If this method is never invoked, then the default value is the "normal" literal.
+      *
+      * @param  value  new style to use for a font, or {@code null} for resetting the default value.
+      */
+     public void setStyle(final Expression<R,String> value) {
+         style = value;
+     }
+ 
+     /**
+      * Returns the amount of weight or boldness to use for a font.
+      * The allowed values are "normal" and "bold".
+      *
+      * @return amount of weight or boldness to use for a font.
+      */
+     public Expression<R,String> getWeight() {
+         final var value = weight;
+         return (value != null) ? value : factory.normal;
+     }
+ 
+     /**
+      * Sets the amount of weight or boldness to use for a font.
+      * If this method is never invoked, then the default value is the "normal" literal.
+      *
+      * @param  value  new amount of weight to use for a font, or {@code null} for resetting the default value.
+      */
+     public void setWeight(final Expression<R,String> value) {
+         weight = value;
+     }
+ 
+     /**
+      * Returns the size (in pixels) to use for the font.
+      *
+      * @return size (in pixels) to use for the font.
+      */
+     public Expression<R, ? extends Number> getSize() {
+         final var value = size;
+         return (value != null) ? value : factory.ten;
+     }
+ 
+     /**
+      * Sets the size (in pixels) to use for the font.
+      * If this method is never invoked, then the default value is 10 pixels.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new size to use for the font, or {@code null} for resetting the default value.
+      */
+     public void setSize(final Expression<R, ? extends Number> value) {
+         size = value;
+     }
+ 
+     /*
+      * TODO: we need a private method like below for formatting above SVG parameters:
+      * See Stroke for more detais.
+      */
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {family, style, weight, size};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Font<R> clone() {
+         final var clone = (Font<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         family = new ArrayList<>(family);
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
index 0000000000,4864a81be2..e2fd39bd6b
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Graphic.java
@@@ -1,0 -1,351 +1,351 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.List;
+ import java.util.ArrayList;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlElements;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * A "graphic symbol" with an inherent shape, color(s), and possibly size.
+  * A "graphic" can be very informally defined as "a little picture"
+  * and can be of either a raster or vector-graphic source type.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "GraphicType", propOrder = {
+     "graphicalSymbols",
+     "opacity",
+     "size",
+     "rotation",
+     "anchorPoint",
+     "displacement"
+ })
+ @XmlRootElement(name = "Graphic")
+ public class Graphic<R> extends StyleElement<R> implements Translucent<R> {
+     /**
+      * List of external image files or marks that comprise this graphic.
+      * All elements of the list must be instances of either {@link Mark} or {@link ExternalGraphic}.
+      * If empty it is to be treated a single default mark.
+      *
+      * @see #graphicalSymbols()
+      */
+     @XmlElements({
+         @XmlElement(name = "Mark", type = Mark.class),
+         @XmlElement(name = "ExternalGraphic", type = ExternalGraphic.class)
+     })
+     private List<GraphicalSymbol<R>> graphicalSymbols;
+ 
+     /**
+      * Level of translucency as a floating point number, or {@code null} for the default value.
+      *
+      * @see #getOpacity()
+      * @see #setOpacity(Expression)
+      *
+      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+      */
+     @XmlElement(name = "Opacity")
+     protected Expression<R, ? extends Number> opacity;
+ 
+     /**
+      * Absolute size of the graphic as a floating point number, or {@code null} for the default value.
+      *
+      * @see #getSize()
+      * @see #setSize(Expression)
+      *
+      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+      */
+     @XmlElement(name = "Size")
+     protected Expression<R, ? extends Number> size;
+ 
+     /**
+      * Rotation angle of the graphic when it is drawn, or {@code null} for the default value.
+      *
+      * @see #getRotation()
+      * @see #setRotation(Expression)
+      *
+      * @todo Needs a JAXB adapter to {@code ParameterValueType}.
+      */
+     @XmlElement(name = "Rotation")
+     protected Expression<R, ? extends Number> rotation;
+ 
+     /**
+      * Location to use for anchoring the graphic to the geometry, or {@code null} for lazily constructed default.
+      *
+      * @see #getAnchorPoint()
+      * @see #setAnchorPoint(AnchorPoint)
+      */
+     @XmlElement(name = "AnchorPoint")
+     protected AnchorPoint<R> anchorPoint;
+ 
+     /**
+      * Displacement from the "hot-spot" point, or {@code null} for lazily constructed default.
+      *
+      * @see #getDisplacement()
+      * @see #setDisplacement(Displacement)
+      */
+     @XmlElement(name = "Displacement")
+     protected Displacement<R> displacement;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Graphic() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a graphic initialized to opaque default mark, default size and no rotation.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Graphic(final StyleFactory<R> factory) {
+         super(factory);
+         graphicalSymbols = new ArrayList<>();
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Graphic(final Graphic<R> source) {
+         super(source);
+         graphicalSymbols = new ArrayList<>(source.graphicalSymbols);
+         opacity          = source.opacity;
+         size             = source.size;
+         rotation         = source.rotation;
+         anchorPoint      = source.anchorPoint;
+         displacement     = source.displacement;
+     }
+ 
+     /**
+      * Returns the list of external image files or marks that comprise this graphic.
+      * All elements of the list shall be instances of either {@link Mark} or {@link ExternalGraphic}.
+      * The list may contain multiple external URLs and marks with the semantic that they all provide
+      * the equivalent graphic in different {@linkplain GraphicalSymbol#getFormat() formats}.
+      *
+      * <p>If the list is empty, it is to be treated as a single default mark.
+      * That default is a square with with a 50%-gray fill and a black outline,
+      * with a size of 6 pixels, unless an explicit {@linkplain #getSize() size} is specified.</p>
+      *
+      * <p>The returned collection is <em>live</em>:
+      * changes in that collection are reflected into this object, and conversely.</p>
+      *
+      * @return list of marks or external graphics, as a live collection.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public List<GraphicalSymbol<R>> graphicalSymbols() {
+         return graphicalSymbols;
+     }
+ 
+     /**
+      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+      *
+      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+      *
+      * @see Fill#getOpacity()
+      * @see Stroke#getOpacity()
+      * @see RasterSymbolizer#getOpacity()
+      */
+     @Override
+     public Expression<R, ? extends Number> getOpacity() {
+         return defaultToOne(opacity);
+     }
+ 
+     /**
+      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * If this method is never invoked, then the default value is literal 1 (totally opaque).
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new level of translucency, or {@code null} for resetting the default value.
+      */
+     @Override
+     public void setOpacity(final Expression<R, ? extends Number> value) {
+         opacity = value;
+     }
+ 
+     /**
+      * Returns the absolute size of the graphic as a floating point number.
+      * If a size is specified, the height of the graphic will be scaled to
+      * that size and the aspect ratio will be used for computing the width.
+      * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+      * Value shall be positive.
+      *
+      * <h4>Default value</h4>
+      * The default size of an image format is the inherent size of the image.
+      * The default size of an image format without an inherent size is defined
+      * to be 16 pixels in height and the corresponding aspect in width.
+      * If no image or mark is specified, then the size of the default mark is 6 pixels.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @return absolute size of the graphic as a floating point number, or {@code null} for the default value.
+      */
+     public Expression<R, ? extends Number> getSize() {
+         final var value = size;
+         if (value == null && graphicalSymbols.isEmpty()) {
+             return factory.six;
+         }
+         return value;
+     }
+ 
+     /**
+      * Sets the absolute size of the graphic as a floating point number.
+      * If this method is never invoked, then the default value is {@code null}.
+      *
+      * @param  value  new absolute size of the graphic, or {@code null} for the default value.
+      */
+     public void setSize(final Expression<R, ? extends Number> value) {
+         size = value;
+     }
+ 
+     /**
+      * Returns the rotation angle of the graphic when it is drawn.
+      * The rotation is in the clockwise direction around the graphic center point in decimal degrees.
+      * Negative values mean counter-clockwise rotation. The default value is 0.0 (no rotation).
+      *
+      * <p>The point within the graphic about which it is rotated is format dependent.
+      * If a format does not include an inherent rotation point,
+      * then the point of rotation should be the centroid.</p>
+      *
+      * @return the rotation angle of the graphic when it is drawn.
+      */
+     public Expression<R, ? extends Number> getRotation() {
+         return defaultToZero(rotation);
+     }
+ 
+     /**
+      * Sets the rotation angle of the graphic when it is drawn.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new rotation angle of the graphic, or {@code null} for resetting the default value.
+      */
+     public void setRotation(final Expression<R, ? extends Number> value) {
+         rotation = value;
+     }
+ 
+     /**
+      * Returns the location inside of a graphic to use for anchoring the graphic to the main-geometry point.
+      * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers
+      * relative the the graphic bounding box, where (0,0) is the lower-left corner and
+      * (1,1) is the upper-right corner.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this graphic, and conversely.</p>
+      *
+      * @return the anchor point.
+      */
+     public AnchorPoint<R> getAnchorPoint() {
+         if (anchorPoint == null) {
+             anchorPoint = factory.createAnchorPoint();
+         }
+         return anchorPoint;
+     }
+ 
+     /**
+      * Sets the location inside of a graphic to use for anchoring the graphic to the main-geometry point.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain AnchorPoint#AnchorPoint() default anchor point}.
+      *
+      * @param  value  new anchor point, or {@code null} for resetting the default value.
+      */
+     public void setAnchorPoint(final AnchorPoint<R> value) {
+         anchorPoint = value;
+     }
+ 
+     /**
+      * Returns the two-dimensional displacement from the "hot-spot" point.
+      * This element may be used to avoid over-plotting of multiple graphic symbols for the same point.
+      * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+      * Positive values are to the right of the point.
+      * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+      *
+      * <p>If displacement is used in conjunction with size and/or rotation
+      * then the graphic symbol shall be scaled and/or rotated before it is displaced.</p>
+      *
+      * @return displacement from the "hot-spot" point.
+      *
+      * @see PolygonSymbolizer#getDisplacement()
+      * @see PointPlacement#getDisplacement()
+      */
+     public Displacement<R> getDisplacement() {
+         if (displacement == null) {
+             displacement = factory.createDisplacement();
+         }
+         return displacement;
+     }
+ 
+     /**
+      * Sets the two-dimensional displacement from the "hot-spot" point.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+      *
+      * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
+      */
+     public void setDisplacement(final Displacement<R> value) {
+         displacement = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {graphicalSymbols, opacity, size, rotation, anchorPoint, displacement};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Graphic<R> clone() {
+         final var clone = (Graphic<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         graphicalSymbols = new ArrayList<>(graphicalSymbols);
+         graphicalSymbols.replaceAll(GraphicalSymbol::clone);
+         if (anchorPoint  != null) anchorPoint  = anchorPoint .clone();
+         if (displacement != null) displacement = displacement.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
index 0000000000,7d528f9a28..c1503dadcd
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/GraphicStroke.java
@@@ -1,0 -1,203 +1,203 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Repeated-linear-graphic stroke.
+  * A graphic stroke can only be used by a {@link Stroke}.
+  * This class contains a {@link Graphic} property,
+  * but its encapsulation in {@code GraphicStroke} means that
+  * the graphic should be bent around the curves of the line string.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "GraphicStrokeType", propOrder = {
+     "graphic",
+     "initialGap",
+     "gap"
+ })
+ @XmlRootElement(name = "GraphicStroke")
+ public class GraphicStroke<R> extends StyleElement<R> implements GraphicalElement<R> {
+     /**
+      * The graphic to be repeated, or {@code null} for lazily constructed default.
+      * This property is mandatory: a null value <em>shall</em> be replaced by a default value when first requested.
+      *
+      * @see #getGraphic()
+      * @see #setGraphic(Graphic)
+      */
+     protected Graphic<R> graphic;
+ 
+     /**
+      * How far away the first graphic will be drawn, or {@code null} for the default value.
+      *
+      * @see #getInitialGap()
+      * @see #setInitialGap(Expression)
+      */
+     @XmlElement(name = "InitialGap")
+     protected Expression<R, ? extends Number> initialGap;
+ 
+     /**
+      * Distance between two graphics, or {@code null} for the default value.
+      *
+      * @see #getGap()
+      * @see #setGap(Expression)
+      */
+     @XmlElement(name = "Gap")
+     protected Expression<R, ? extends Number> gap;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private GraphicStroke() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a graphic stroke initialized to a default graphic and no gap.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public GraphicStroke(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public GraphicStroke(final GraphicStroke<R> source) {
+         super(source);
+         graphic    = source.graphic;
+         initialGap = source.initialGap;
+         gap        = source.gap;
+     }
+ 
+     /**
+      * Returns the graphic to be repeated.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.
+      *
+      * @return the graphic to be repeated.
+      *
+      * @see GraphicFill#getGraphic()
+      */
+     @Override
+     @XmlElement(name = "Graphic", required = true)
+     public final Graphic<R> getGraphic() {
+         if (graphic == null) {
+             graphic = factory.createGraphic();
+         }
+         return graphic;
+     }
+ 
+     /**
+      * Sets the graphic to be repeated.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is a {@linkplain Graphic#Graphic() default instance}.
+      *
+      * @param  value  new graphic, or {@code null} for resetting the default value.
+      *
+      * @see GraphicFill#setGraphic(Graphic)
+      */
+     @Override
+     public final void setGraphic(final Graphic<R> value) {
+         graphic = value;
+     }
+ 
+     /**
+      * Returns how far away the first graphic will be drawn relative to the start of the rendering line.
+      *
+      * @return distance of first graphic relative to the rendering start.
+      */
+     public Expression<R, ? extends Number> getInitialGap() {
+         return defaultToZero(initialGap);
+     }
+ 
+     /**
+      * Sets how far away the first graphic will be drawn relative to the start of the rendering line.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
+      */
+     public void setInitialGap(final Expression<R, ? extends Number> value) {
+         initialGap = value;
+     }
+ 
+     /**
+      * Returns the distance between two graphics.
+      *
+      * @return distance between two graphics.
+      */
+     public Expression<R, ? extends Number> getGap() {
+         return defaultToZero(gap);
+     }
+ 
+     /**
+      * Sets the distance between two graphics.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new distance between two graphics, or {@code null} for resetting the default value.
+      */
+     public void setGap(final Expression<R, ? extends Number> value) {
+         gap = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {graphic, initialGap, gap};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public GraphicStroke<R> clone() {
+         final var clone = (GraphicStroke<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (graphic != null) graphic = graphic.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
index 0000000000,1639204ca7..6a456f75e8
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Halo.java
@@@ -1,0 -1,170 +1,170 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Fill that is applied to the backgrounds of font glyphs.
+  * The use of halos improves the readability of text labels.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "HaloType", propOrder = {
+     "radius",
+     "fill"
+ })
+ @XmlRootElement(name = "Halo")
+ public class Halo<R> extends StyleElement<R> {
+     /**
+      * Radius (in pixels) of the  the halo around the text, or {@code null} for the default value.
+      *
+      * @see #getRadius()
+      * @see #setRadius(Expression)
+      */
+     @XmlElement(name = "Radius")
+     protected Expression<R, ? extends Number> radius;
+ 
+     /**
+      * How the halo area around the text should be filled, or {@code null} for the default value.
+      *
+      * @see #getFill()
+      * @see #setFill(Fill)
+      */
+     @XmlElement(name = "Fill")
+     protected Fill<R> fill;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Halo() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an halo initialized to a white color and a radius of 1 pixel.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Halo(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Halo(final Halo<R> source) {
+         super(source);
+         fill   = source.fill;
+         radius = source.radius;
+     }
+ 
+     /**
+      * Returns the expression fetching the pixel radius of the halo around the text.
+      * This is the absolute size of a halo radius in pixels.
+      * It extends the area to the outside edge of glyphs and the inside edge of "holes" in the glyphs.
+      * Negative values are not allowed.
+      *
+      * @return radius (in pixels) of the  the halo around the text.
+      */
+     public Expression<R, ? extends Number> getRadius() {
+         return defaultToOne(radius);
+     }
+ 
+     /**
+      * Sets the expression fetching the pixel radius of the halo around the text.
+      * If this method is never invoked, then the default value is 1 pixel.
+      *
+      * @param  value  new radius (in pixels), or {@code null} for resetting the default value.
+      */
+     public void setRadius(Expression<R, ? extends Number> value) {
+         radius = value;
+     }
+ 
+     /**
+      * Returns the fill for the halo area around the text.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this halo, and conversely.
+      *
+      * @return graphic, color and opacity of the text to draw.
+      */
+     public Fill<R> getFill() {
+         if (fill == null) {
+             fill = factory.createFill();
+             fill.setColor(factory.white);
+         }
+         return fill;
+     }
+ 
+     /**
+      * Sets the fill for the halo area around the text.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is a solid white color.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new fill of the text to draw, or {@code null} for resetting the default value.
+      */
+     public void setFill(final Fill<R> value) {
+         fill = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {radius, fill};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Halo<R> clone() {
+         final var clone = (Halo<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (fill != null) fill = fill.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
index 0000000000,2e6d001ceb..3da4712138
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LinePlacement.java
@@@ -1,0 -1,283 +1,283 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about where and how a text label should be rendered relative to a line.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Ian Turton (CCG)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "LinePlacementType", propOrder = {
+     "perpendicularOffset",
+     "isRepeated",
+     "initialGap",
+     "gap",
+     "isAligned",
+     "generalizeLine"
+ })
+ @XmlRootElement(name = "LinePlacement")
+ public class LinePlacement<R> extends LabelPlacement<R> {
+     /**
+      * Perpendicular distance away from a line where to draw a label, or {@code null} for the default value.
+      *
+      * @see #getPerpendicularOffset()
+      * @see #setPerpendicularOffset(Expression)
+      */
+     @XmlElement(name = "PerpendicularOffset")
+     protected Expression<R, ? extends Number> perpendicularOffset;
+ 
+     /**
+      * Whether the label will be repeatedly drawn along the line, or {@code null} for the default value.
+      *
+      * @see #isRepeated()
+      * @see #setRepeated(boolean)
+      *
+      * @todo Needs an adapter from expression to plain boolean.
+      */
+     @XmlElement(name = "IsRepeated")
+     protected Expression<R,Boolean> isRepeated;
+ 
+     /**
+      * How far away the first label will be drawn, or {@code null} for the default value.
+      *
+      * @see #getInitialGap()
+      * @see #setInitialGap(Expression)
+      */
+     @XmlElement(name = "InitialGap")
+     protected Expression<R, ? extends Number> initialGap;
+ 
+     /**
+      * Distance between two labels, or {@code null} for the default value.
+      *
+      * @see #getGap()
+      * @see #setGap(Expression)
+      */
+     @XmlElement(name = "Gap")
+     protected Expression<R, ? extends Number> gap;
+ 
+     /**
+      * Whether labels are aligned to the line geometry, or {@code null} for the default value.
+      * If false, labels are drawn horizontally.
+      *
+      * @see #isAligned()
+      * @see #setAligned(Expression)
+      *
+      * @todo Needs an adapter from expression to plain boolean.
+      */
+     @XmlElement(name = "IsAligned")
+     protected Expression<R,Boolean> isAligned;
+ 
+     /**
+      * Whether to allow the geometry to be generalized, or {@code null} for the default value.
+      *
+      * @see #getGeneralizeLine()
+      * @see #setGeneralizeLine(Expression)
+      *
+      * @todo Needs an adapter from expression to plain boolean.
+      */
+     @XmlElement(name = "GeneralizeLine")
+     protected Expression<R,Boolean> generalizeLine;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private LinePlacement() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a line placement initialized to no offset, no repetition and no gap.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public LinePlacement(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public LinePlacement(final LinePlacement<R> source) {
+         super(source);
+         perpendicularOffset = source.perpendicularOffset;
+         isRepeated          = source.isRepeated;
+         initialGap          = source.initialGap;
+         gap                 = source.gap;
+         isAligned           = source.isAligned;
+         generalizeLine      = source.generalizeLine;
+     }
+ 
+     /**
+      * Returns a perpendicular distance away from a line where to draw a label.
+      * The distance units of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+      * The value is positive to the left-hand side of the line string.
+      * Negative numbers mean right. The default offset is 0.
+      *
+      * @return perpendicular distance away from a line where to draw a label.
+      */
+     public Expression<R, ? extends Number> getPerpendicularOffset() {
+         return defaultToZero(perpendicularOffset);
+     }
+ 
+     /**
+      * Sets a perpendicular distance away from a line where to draw a label.
+      * If this method is never invoked, then the default value is literal 0.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new distance to apply for drawing label, or {@code null} for resetting the default value.
+      */
+     public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
+         perpendicularOffset = value;
+     }
+ 
+     /**
+      * Returns whether the label will be repeatedly drawn along the line.
+      * If {@code true}, the repetition will use the {@linkplain #getInitialGap() initial gap}
+      * and {@linkplain #getGap() gap} for defining the spaces at the beginning and between labels.
+      *
+      * @return whether the label will be repeatedly drawn along the line.
+      */
+     public Expression<R,Boolean> isRepeated() {
+         return defaultToFalse(isRepeated);
+     }
+ 
+     /**
+      * Sets whether the label will be repeatedly drawn along the line.
+      * If this method is never invoked, then the default value is literal false.
+      *
+      * @param  value  whether the label will be repeated, or {@code null} for resetting the default value.
+      */
+     public void setRepeated(final Expression<R,Boolean> value) {
+         isRepeated = value;
+     }
+ 
+     /**
+      * Returns how far away the first label will be drawn relative to the start of the rendering line.
+      *
+      * @return distance of first label relative to the rendering start.
+      */
+     public Expression<R, ? extends Number> getInitialGap() {
+         return defaultToZero(initialGap);
+     }
+ 
+     /**
+      * Sets how far away the first label will be drawn relative to the start of the rendering line.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new distance relative to rendering start, or {@code null} for resetting the default value.
+      */
+     public void setInitialGap(final Expression<R, ? extends Number> value) {
+         initialGap = value;
+     }
+ 
+     /**
+      * Returns the distance between two labels.
+      *
+      * @return distance between two labels.
+      */
+     public Expression<R, ? extends Number> getGap() {
+         return defaultToZero(gap);
+     }
+ 
+     /**
+      * Sets the distance between two labels.
+      * If this method is never invoked, then the default value is literal 0.
+      *
+      * @param  value  new distance between two labels, or {@code null} for resetting the default value.
+      */
+     public void setGap(final Expression<R, ? extends Number> value) {
+         gap = value;
+     }
+ 
+     /**
+      * Returns whether labels are aligned to the line geometry or drawn horizontally.
+      *
+      * @return whether labels are aligned to the line geometry or drawn horizontally.
+      */
+     public Expression<R,Boolean> isAligned() {
+         return defaultToTrue(isAligned);
+     }
+ 
+     /**
+      * Sets whether labels are aligned to the line geometry or drawn horizontally.
+      * If this method is never invoked, then the default value is literal true.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  whether labels are aligned to the line geometry, or {@code null} for resetting the default value.
+      */
+     public void setAligned(final Expression<R,Boolean> value) {
+         isAligned = value;
+     }
+ 
+     /**
+      * Returns whether to allow the geometry to be generalized for label placement.
+      *
+      * @return whether to allow the geometry to be generalized for label placement.
+      */
+     public Expression<R,Boolean> getGeneralizeLine() {
+         return defaultToFalse(generalizeLine);
+     }
+ 
+     /**
+      * Sets whether to allow the geometry to be generalized for label placement.
+      * If this method is never invoked, then the default value is literal false.
+      *
+      * @param  value whether to allow the geometry to be generalized, or {@code null} for resetting the default value.
+      */
+     public void setGeneralizeLine(final Expression<R,Boolean> value) {
+         generalizeLine = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {perpendicularOffset, isRepeated, initialGap, gap, isAligned, generalizeLine};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public LinePlacement<R> clone() {
+         return (LinePlacement<R>) super.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
index 0000000000,2119f059dc..8445f212db
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/LineSymbolizer.java
@@@ -1,0 -1,173 +1,173 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to draw on a map the lines of a geometry.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "LineSymbolizerType", propOrder = {
+     "stroke",
+     "perpendicularOffset"
+ })
+ @XmlRootElement(name = "LineSymbolizer")
+ public class LineSymbolizer<R> extends Symbolizer<R> {
+     /**
+      * Information about how to draw lines, or {@code null} for lazily constructed default.
+      *
+      * @see #getStroke()
+      * @see #setStroke(Stroke)
+      */
+     @XmlElement(name = "Stroke")
+     protected Stroke<R> stroke;
+ 
+     /**
+      * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
+      *
+      * @see #getPerpendicularOffset()
+      * @see #setPerpendicularOffset(Expression)
+      */
+     @XmlElement(name = "PerpendicularOffset")
+     protected Expression<R, ? extends Number> perpendicularOffset;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private LineSymbolizer() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a line symbolizer with the default stroke and no perpendicular offset.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public LineSymbolizer(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public LineSymbolizer(final LineSymbolizer<R> source) {
+         super(source);
+         stroke = source.stroke;
+         perpendicularOffset = source.perpendicularOffset;
+     }
+ 
+     /**
+      * Returns the object containing all the information necessary to draw styled lines.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.
+      *
+      * @return information about how to draw lines.
+      */
+     public Stroke<R> getStroke() {
+         if (stroke == null) {
+             stroke = factory.createStroke();
+         }
+         return stroke;
+     }
+ 
+     /**
+      * Sets the object containing all the information necessary to draw styled lines.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is a {@linkplain Stroke#Stroke() default instance}.
+      *
+      * @param  value  new information about how to draw lines, or {@code null} for resetting the default value.
+      */
+     public void setStroke(final Stroke<R> value) {
+         stroke = value;
+     }
+ 
+     /**
+      * Returns a distance to apply for drawing lines in parallel to the original geometry.
+      * These parallel lines have to be constructed so that the distance between
+      * original geometry and drawn line stays equal.
+      * This construction may result in drawn lines that are
+      * actually smaller or longer than the original geometry.
+      *
+      * <p>The distance units of measurement is given by {@link #getUnitOfMeasure()}.
+      * The value is positive to the left-hand side of the line string.
+      * Negative numbers mean right. The default offset is 0.</p>
+      *
+      * @return distance to apply for drawing lines in parallel to the original geometry.
+      */
+     public Expression<R, ? extends Number> getPerpendicularOffset() {
+         return defaultToZero(perpendicularOffset);
+     }
+ 
+     /**
+      * Sets a distance to apply for drawing lines in parallel to the original geometry.
+      * If this method is never invoked, then the default value is literal 0.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
+      */
+     public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
+         perpendicularOffset = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {perpendicularOffset, stroke};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public LineSymbolizer<R> clone() {
+         final var clone = (LineSymbolizer<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         stroke = stroke.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
index 0000000000,bb97c23182..c155f61788
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Mark.java
@@@ -1,0 -1,293 +1,293 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Optional;
+ import jakarta.xml.bind.Marshaller;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Predefined shapes that can be drawn at the points of the geometry.
+  * This is an alternative to {@link ExternalGraphic} for
+  * {@linkplain Graphic#graphicalSymbols graphical symbols}.
+  * When marks are provided in the bottom of the graphical symbol list,
+  * it allows a style to be specified that can produce a usable result in a best-effort basis.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "MarkType", propOrder = {
+     "wellKnownName",
+ //  "onlineResource",       // XML encoding not yet available.
+ //  "inlineContent",        // Idem.
+     "format",
+     "markIndex",
+     "fill",
+     "stroke"
+ })
+ @XmlRootElement(name = "Mark")
+ public class Mark<R> extends GraphicalSymbol<R> {
+     /**
+      * Expression whose value will indicate the symbol to draw, or {@code null} for the default value.
+      *
+      * @see #getWellKnownName()
+      * @see #setWellKnownName(Expression)
+      */
+     @XmlElement(name = "WellKnownName")
+     protected Expression<R,String> wellKnownName;
+ 
+     /**
+      * Information about how the interior of marks should be filled, or {@code null} for no fill.
+      * If no value has been explicitly set (including null value),
+      * then a default fill will be lazily created when first requested.
+      *
+      * @see #getFill()
+      * @see #setFill(Fill)
+      */
+     @XmlElement(name = "Fill")
+     protected Fill<R> fill;
+ 
+     /**
+      * Whether {@link #fill} has been explicitly set to some value, including null.
+      * If {@code false}, then a default fill will be created when first needed.
+      */
+     private boolean isFillSet;
+ 
+     /**
+      * Information about styled lines, or {@code null} if mark lines should not be drawn.
+      * If no value has been explicitly set (including null value),
+      * then a default stroke will be lazily created when first needed.
+      *
+      * @see #getStroke()
+      * @see #setStroke(Stroke)
+      */
+     @XmlElement(name = "Stroke")
+     protected Stroke<R> stroke;
+ 
+     /**
+      * Whether {@link #stroke} has been explicitly set to some value, including null.
+      * If {@code false}, then a default stroke will be created when first requested.
+      */
+     private boolean isStrokeSet;
+ 
+     /**
+      * Individual mark to select in a mark archive, or {@code null} if none.
+      *
+      * @see #getMarkIndex()
+      * @see #setMarkIndex(Expression)
+      */
+     @XmlElement(name = "MarkIndex")
+     protected Expression<R,Integer> markIndex;
+ 
+     /**
+      * Invoked by JAXB before marshalling this mark.
+      * Creates the default fill and stroke if needed.
+      */
+     private void beforeMarshal(Marshaller caller) {
+         if (fill   == null && !isFillSet)   fill   = factory.createFill();
+         if (stroke == null && !isStrokeSet) stroke = factory.createStroke();
+     }
+ 
+     /**
+      * For JAXB unmarshalling only. This constructor disables the lazy creation of default values.
+      * This is because OGC 05-077r4 said that if the fill or the stroke is not specified,
+      * then no fill or stroke should be applied.
+      */
+     private Mark() {
+         // Thread-local factory will be used.
+         isFillSet   = true;
+         isStrokeSet = true;
+     }
+ 
+     /**
+      * Creates a mark initialized to a gray square with black outline.
+      * The size is specified by {@link Graphic#getSize()} and should be 6 pixels by default.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Mark(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Mark(final Mark<R> source) {
+         super(source);
+         wellKnownName = source.wellKnownName;
+         fill          = source.fill;
+         stroke        = source.stroke;
+     }
+ 
+     /**
+      * Returns the expression whose value will indicate the symbol to draw.
+      * Allowed values include at least "square", "circle", "triangle", "star", "cross", and "x".
+      * Renderings of these marks may be made solid or hollow depending on
+      * {@linkplain #getFill() fill} and {@linkplain #getStroke() stroke} elements.
+      *
+      * <p>The well-known name may be ignored if the mark is also provided
+      * by {@linkplain #getInlineContent() inline content}
+      * or {@linkplain #getOnlineResource() online resource}.</p>
+      *
+      * @return well-known name of the mark to render.
+      *
+      * @see #getOnlineResource()
+      * @see #getInlineContent()
+      */
+     public Expression<R,String> getWellKnownName() {
+         final var value = wellKnownName;
+         return (value != null) ? value : factory.square;
+     }
+ 
+     /**
+      * Sets the expression whose value will indicate the symbol to draw.
+      * If this method is never invoked, then the default value is literal "square".
+      *
+      * @param  value  well-known name of the mark to render, or {@code null} for resetting the default.
+      */
+     public void setWellKnownName(final Expression<R,String> value) {
+         wellKnownName = value;
+     }
+ 
+     /**
+      * Returns the object that indicates how the mark should be filled.
+      * If absent, then the marks are not to be filled at all.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this mark, and conversely.</p>
+      *
+      * @return information about how the interior of polygons should be filled.
+      *
+      * @see #getStroke()
+      */
+     public Optional<Fill<R>> getFill() {
+         if (!isFillSet) {
+             isFillSet = true;
+             fill = factory.createFill();
+         }
+         return Optional.ofNullable(fill);
+     }
+ 
+     /**
+      * Sets information about how the interior of marks should be filled.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is the {@linkplain Fill#Fill() default fill}.
+      *
+      * @param  value  new information about the fill, or {@code null} for no fill.
+      */
+     public void setFill(final Fill<R> value) {
+         isFillSet = true;
+         fill = value;
+     }
+ 
+     /**
+      * Returns the object that indicates how the edges of the mark will be drawn.
+      * This is used for the edges of marks.
+      * Absent means that the edges will not be drawn at all.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this mark, and conversely.</p>
+      *
+      * @return information about styled lines, or {@code null} if lines should not be drawn.
+      *
+      * @see #getFill()
+      */
+     public Optional<Stroke<R>> getStroke() {
+         if (!isStrokeSet) {
+             isStrokeSet = true;
+             stroke = factory.createStroke();
+         }
+         return Optional.ofNullable(stroke);
+     }
+ 
+     /**
+      * Sets information about styled lines.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is the {@linkplain Stroke#Stroke() default stroke}.
+      *
+      * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
+      */
+     public void setStroke(final Stroke<R> value) {
+         isStrokeSet = true;
+         stroke = value;
+     }
+ 
+     /**
+      * Returns an individual mark to select in a mark archive.
+      * For example it can be the index of a glyph to select in a TrueType fond file.
+      *
+      * @return individual mark to select in a mark archive.
+      */
+     public Optional<Expression<R,Integer>> getMarkIndex() {
+         return Optional.ofNullable(markIndex);
+     }
+ 
+     /**
+      * Sets an individual mark to select in a mark archive.
+      *
+      * @param  value  new index of an individual mark to select, or {@code null} if none.
+      */
+     public void setMarkIndex(final Expression<R,Integer> value) {
+         markIndex = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {wellKnownName, fill, isFillSet, stroke, isStrokeSet, markIndex};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Mark<R> clone() {
+         final var clone = (Mark<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (fill   != null) fill   = fill.clone();
+         if (stroke != null) stroke = stroke.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
index 0000000000,6efa32f95b..e98fcad4b5
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PointPlacement.java
@@@ -1,0 -1,209 +1,209 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how a text label is positioned relative to a point.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Ian Turton (CCG)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "PointPlacementType", propOrder = {
+     "anchorPoint",
+     "displacement",
+     "rotation"
+ })
+ @XmlRootElement(name = "PointPlacement")
+ public class PointPlacement<R> extends LabelPlacement<R> {
+     /**
+      * Location to use for anchoring the label to the point, or {@code null} for lazily constructed default.
+      *
+      * @see #getAnchorPoint()
+      * @see #setAnchorPoint(AnchorPoint)
+      */
+     @XmlElement(name = "AnchorPoint")
+     protected AnchorPoint<R> anchorPoint;
+ 
+     /**
+      * Two-dimensional displacements from the "hot-spot" point, or {@code null} for lazily constructed default.
+      *
+      * @see #getDisplacement()
+      * @see #setDisplacement(Displacement)
+      */
+     @XmlElement(name = "Displacement")
+     protected Displacement<R> displacement;
+ 
+     /**
+      * Expression fetching the rotation of the label when it is drawn.
+      *
+      * @see #getRotation()
+      * @see #setRotation(Expression)
+      */
+     @XmlElement(name = "Rotation")
+     protected Expression<R, ? extends Number> rotation;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private PointPlacement() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a point placement initialized to anchor at the middle and no displacement.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public PointPlacement(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public PointPlacement(final PointPlacement<R> source) {
+         super(source);
+         anchorPoint  = source.anchorPoint;
+         displacement = source.displacement;
+         rotation     = source.rotation;
+     }
+ 
+     /**
+      * Returns the location inside of a label to use for anchoring the label to the point.
+      * The coordinates are given as (<var>x</var>,<var>y</var>) floating-point numbers
+      * relative the the label bounding box, where (0,0) is the lower-left corner and
+      * (1,1) is the upper-right corner.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this graphic, and conversely.</p>
+      *
+      * @return the anchor point.
+      */
+     public AnchorPoint<R> getAnchorPoint() {
+         if (anchorPoint == null) {
+             anchorPoint = factory.createAnchorPoint();
+         }
+         return anchorPoint;
+     }
+ 
+     /**
+      * Sets the location inside of a label to use for anchoring the label to the point.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain AnchorPoint#AnchorPoint() default anchor point}.
+      *
+      * @param  value  new anchor point, or {@code null} for resetting the default value.
+      */
+     public void setAnchorPoint(final AnchorPoint<R> value) {
+         anchorPoint = value;
+     }
+ 
+     /**
+      * Returns the two-dimensional displacements from the "hot-spot" point.
+      * The displacements are in units of pixels above and to the right of the point.
+      *
+      * @return two-dimensional displacements from the "hot-spot" point.
+      *
+      * @see Graphic#getDisplacement()
+      * @see PolygonSymbolizer#getDisplacement()
+      */
+     public Displacement<R> getDisplacement() {
+         if (displacement == null) {
+             displacement = factory.createDisplacement();
+         }
+         return displacement;
+     }
+ 
+     /**
+      * Sets the two-dimensional displacements from the "hot-spot" point.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+      *
+      * @param  value  new displacements from the "hot-spot" point, or {@code null} for resetting the default value.
+      */
+     public void setDisplacement(final Displacement<R> value) {
+         displacement = value;
+     }
+ 
+     /**
+      * Returns the expression fetching the rotation of the label when it is drawn.
+      * This is the clockwise rotation of the label in degrees from the normal direction for a font
+      * (left-to-right for Latin languages).
+      *
+      * @return rotation of the label when it is drawn.
+      */
+     public Expression<R, ? extends Number> getRotation() {
+         return defaultToZero(rotation);
+     }
+ 
+     /**
+      * Sets the expression fetching the rotation of the label when it is drawn.
+      * If this method is never invoked, then the default value is literal zero.
+      *
+      * @param  value  new rotation of the label, or {@code null} for resetting the default value.
+      */
+     public void setRotation(final Expression<R, ? extends Number> value) {
+         rotation = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {anchorPoint, displacement, rotation};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public PointPlacement<R> clone() {
+         final var clone = (PointPlacement<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (anchorPoint  != null) anchorPoint  = anchorPoint .clone();
+         if (displacement != null) displacement = displacement.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
index 0000000000,2a3e6acbc2..6b2474f044
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/PolygonSymbolizer.java
@@@ -1,0 -1,324 +1,324 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Optional;
+ import jakarta.xml.bind.Marshaller;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to draw on a map the lines and the interior of polygons.
+  * Holes are not filled but borders around the holes are stroked.
+  * Islands within holes are filled and stroked, and so on.
+  *
+  * <p>If the geometry is not a polygon, then
+  * line strings should be closed for filling but not for stroking.
+  * Points should be rendered as small squares, and
+  * rasters should be rendered using the coverage extent as the polygon.</p>
+  *
+  * <p>The fill should be rendered first, then the stroke should be rendered on top of the fill.
+  * A missing stroke element means that the geometry will not be stroked.</p>
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "PolygonSymbolizerType", propOrder = {
+     "fill",
+     "stroke",
+     "displacement",
+     "perpendicularOffset"
+ })
+ @XmlRootElement(name = "PolygonSymbolizer")
+ public class PolygonSymbolizer<R> extends Symbolizer<R> {
+     /**
+      * Information about how the interior of polygons should be filled, or {@code null} for no fill.
+      * If no value has been explicitly set (including null value),
+      * then a default fill will be lazily created when first requested.
+      *
+      * @see #getFill()
+      * @see #setFill(Fill)
+      */
+     @XmlElement(name = "Fill")
+     protected Fill<R> fill;
+ 
+     /**
+      * Whether {@link #fill} has been explicitly set to some value, including null.
+      * If {@code false}, then a default fill will be created when first needed.
+      */
+     private boolean isFillSet;
+ 
+     /**
+      * Information about styled lines, or {@code null} if lines should not be drawn.
+      * If no value has been explicitly set (including null value),
+      * then a default stroke will be lazily created when first needed.
+      *
+      * @see #getStroke()
+      * @see #setStroke(Stroke)
+      */
+     @XmlElement(name = "Stroke")
+     protected Stroke<R> stroke;
+ 
+     /**
+      * Whether {@link #stroke} has been explicitly set to some value, including null.
+      * If {@code false}, then a default stroke will be created when first requested.
+      */
+     private boolean isStrokeSet;
+ 
+     /**
+      * Displacement from the "hot-spot" point, or {@code null} for lazily constructed default.
+      *
+      * @see #getDisplacement()
+      * @see #setDisplacement(Displacement)
+      */
+     @XmlElement(name = "Displacement")
+     protected Displacement<R> displacement;
+ 
+     /**
+      * Distance to apply for drawing lines in parallel to geometry, or {@code null} for the default value.
+      *
+      * @see #getPerpendicularOffset()
+      * @see #setPerpendicularOffset(Expression)
+      */
+     @XmlElement(name = "PerpendicularOffset")
+     protected Expression<R, ? extends Number> perpendicularOffset;
+ 
+     /**
+      * Invoked by JAXB before marshalling this polyogon symbolizer.
+      * Creates the default fill and stroke if needed.
+      */
+     private void beforeMarshal(Marshaller caller) {
+         if (fill   == null && !isFillSet)   fill   = factory.createFill();
+         if (stroke == null && !isStrokeSet) stroke = factory.createStroke();
+     }
+ 
+     /**
+      * For JAXB unmarshalling only. This constructor disables the lazy creation of default values.
+      * This is because OGC 05-077r4 said that if the fill or the stroke is not specified,
+      * then no fill or stroke should be applied.
+      */
+     private PolygonSymbolizer() {
+         // Thread-local factory will be used.
+         isFillSet   = true;
+         isStrokeSet = true;
+     }
+ 
+     /**
+      * Creates a polygon symbolizer initialized to the default fill and default stroke.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public PolygonSymbolizer(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public PolygonSymbolizer(final PolygonSymbolizer<R> source) {
+         super(source);
+         fill                = source.fill;
+         stroke              = source.stroke;
+         isFillSet           = source.isFillSet;
+         isStrokeSet         = source.isStrokeSet;
+         displacement        = source.displacement;
+         perpendicularOffset = source.perpendicularOffset;
+     }
+ 
+     /**
+      * Returns information about how the interior of polygons should be filled.
+      * If absent, then the polygons are not to be filled at all.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return information about how the interior of polygons should be filled.
+      *
+      * @see #getStroke()
+      * @see #isVisible()
+      */
+     public Optional<Fill<R>> getFill() {
+         if (!isFillSet) {
+             isFillSet = true;
+             fill = factory.createFill();
+         }
+         return Optional.ofNullable(fill);
+     }
+ 
+     /**
+      * Sets information about how the interior of polygons should be filled.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is the {@linkplain Fill#Fill() default fill}.
+      *
+      * @param  value  new information about the fill, or {@code null} for no fill.
+      */
+     public void setFill(final Fill<R> value) {
+         isFillSet = true;
+         fill = value;
+     }
+ 
+     /**
+      * Returns information about styled lines.
+      * This is used for the edges of polygons.
+      * Absent means that lines should not be drawn
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return information about styled lines.
+      *
+      * @see #getFill()
+      * @see #isVisible()
+      */
+     public Optional<Stroke<R>> getStroke() {
+         if (!isStrokeSet) {
+             isStrokeSet = true;
+             stroke = factory.createStroke();
+         }
+         return Optional.ofNullable(stroke);
+     }
+ 
+     /**
+      * Sets information about styled lines.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is the {@linkplain Stroke#Stroke() default stroke}.
+      *
+      * @param  value  new information about styled lines, or {@code null} if lines should not be drawn.
+      */
+     public void setStroke(final Stroke<R> value) {
+         isStrokeSet = true;
+         stroke = value;
+     }
+ 
+     /**
+      * Returns the two-dimensional displacement from the "hot-spot" point.
+      * This element may be used to avoid over-plotting of multiple polygons for one geometry.
+      * It may also be used for creating shadows of polygon geometries.
+      * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+      * Positive values are to the right of the point.
+      * The default displacement is <var>x</var> = 0, <var>y</var> = 0,
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return displacement from the "hot-spot" point.
+      *
+      * @see Graphic#getDisplacement()
+      * @see PointPlacement#getDisplacement()
+      */
+     public Displacement<R> getDisplacement() {
+         if (displacement == null) {
+             displacement = factory.createDisplacement();
+         }
+         return displacement;
+     }
+ 
+     /**
+      * Sets the two-dimensional displacement from the "hot-spot" point.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain Displacement#Displacement() default displacement}.
+      *
+      * @param  value  new displacement from the "hot-spot" point, or {@code null} for resetting the default value.
+      */
+     public void setDisplacement(final Displacement<R> value) {
+         displacement = value;
+     }
+ 
+     /**
+      * Returns a distance to apply for drawing lines in parallel to the original polygon.
+      * This property allow to draw polygons smaller or larger than their actual geometry.
+      * The distance units of measurement is given by {@link #getUnitOfMeasure()}.
+      * The value is positive outside the polygon.
+      *
+      * @return distance to apply for drawing lines in parallel to the original geometry.
+      */
+     public Expression<R, ? extends Number> getPerpendicularOffset() {
+         return defaultToZero(perpendicularOffset);
+     }
+ 
+     /**
+      * Sets a distance to apply for drawing lines in parallel to the original geometry.
+      * If this method is never invoked, then the default value is literal 0.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new distance to apply for drawing lines, or {@code null} for resetting the default value.
+      */
+     public void setPerpendicularOffset(final Expression<R, ? extends Number> value) {
+         perpendicularOffset = value;
+     }
+ 
+     /**
+      * Returns {@code true} if this symbolizer has a fill and/or a stroke.
+      * If both {@code setFill(null)} and {@code setStroke(null)} have been
+      * explicitly invoked with a null argument value, then this method returns {@code false}.
+      *
+      * @return whether this symbolizer has a fill and/or a stroke.
+      *
+      * @see #setFill(Fill)
+      * @see #setStroke(Stroke)
+      */
+     @Override
+     public boolean isVisible() {
+         return fill != null || stroke != null || !(isFillSet | isStrokeSet);
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {fill, isFillSet, stroke, isStrokeSet, displacement, perpendicularOffset};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public PolygonSymbolizer<R> clone() {
+         final var clone = (PolygonSymbolizer<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (stroke       != null) stroke       = stroke.clone();
+         if (fill         != null) fill         = fill.clone();
+         if (displacement != null) displacement = displacement.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
index 0000000000,2cff799d4f..ba873fcce6
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/RasterSymbolizer.java
@@@ -1,0 -1,358 +1,358 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to render raster, matrix or coverage data.
+  * It may be satellite photos or DEMs for example.
+  *
+  * <p>In the particular case of raster symbolizer, {@link #getGeometry()}
+  * should return a {@link org.apache.sis.coverage.BandedCoverage} instead
+  * of a geometry.</p>
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Ian Turton (CCG)
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "RasterSymbolizerType", propOrder = {
+     "opacity",
+     "channelSelection",
+     "overlapBehavior",
+     "colorMap",
+     "contrastEnhancement",
+     "shadedRelief",
+     "imageOutline"
+ })
+ @XmlRootElement(name = "RasterSymbolizer")
+ public class RasterSymbolizer<R> extends Symbolizer<R> implements Translucent<R> {
+     /**
+      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+      * The default value specified by OGC 05-077r4 standard is 1.
+      *
+      * @see #getOpacity()
+      * @see #setOpacity(Expression)
+      */
+     @XmlElement(name = "Opacity")
+     protected Expression<R, ? extends Number> opacity;
+ 
+     /**
+      * Selection of false-color channels for a multi-spectral raster source, or {@code null} if none.
+      *
+      * @see #getChannelSelection()
+      * @see #setChannelSelection(ChannelSelection)
+      */
+     @XmlElement(name = "ChannelSelection")
+     protected ChannelSelection<R> channelSelection;
+ 
+     /**
+      * Behavior when multiple raster images in a layer overlap each other, or {@code null} if unspecified.
+      * The default value is implementation-dependent.
+      *
+      * @see #getOverlapBehavior()
+      * @see #setOverlapBehavior(OverlapBehavior)
+      */
+     @XmlElement(name = "OverlapBehavior")
+     protected OverlapBehavior overlapBehavior;
+ 
+     /**
+      * Mapping of fixed-numeric pixel values to colors, or {@code null} if none.
+      *
+      * @see #getColorMap()
+      * @see #setColorMap(ColorMap)
+      */
+     @XmlElement(name = "ColorMap")
+     protected ColorMap<R> colorMap;
+ 
+     /**
+      * Contrast enhancement for the whole image, or {@code null} if none.
+      *
+      * @see #getContrastEnhancement()
+      * @see #setContrastEnhancement(ContrastEnhancement)
+      */
+     @XmlElement(name = "ContrastEnhancement")
+     protected ContrastEnhancement<R> contrastEnhancement;
+ 
+     /**
+      * Relief shading (or “hill shading”) to apply to the image for a three-dimensional visual effect.
+      *
+      * @see #getShadedRelief()
+      * @see #setShadedRelief(ShadedRelief)
+      */
+     @XmlElement(name = "ShadedRelief")
+     protected ShadedRelief<R> shadedRelief;
+ 
+     /**
+      * Line or polygon symbolizer to use for outlining source rasters, or {@code null} if none.
+      *
+      * @see #getImageOutline()
+      * @see #setImageOutline(Symbolizer)
+      */
+     @XmlElement(name = "ImageOutline")
+     protected Symbolizer<R> imageOutline;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private RasterSymbolizer() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an initially opaque raster symbolizer with no contrast enhancement, shaded relief or outline.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public RasterSymbolizer(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public RasterSymbolizer(final RasterSymbolizer<R> source) {
+         super(source);
+         opacity             = source.opacity;
+         channelSelection    = source.channelSelection;
+         overlapBehavior     = source.overlapBehavior;
+         colorMap            = source.colorMap;
+         contrastEnhancement = source.contrastEnhancement;
+         shadedRelief        = source.shadedRelief;
+         imageOutline        = source.imageOutline;
+     }
+ 
+     /**
+      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+      *
+      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+      *
+      * @see Fill#getOpacity()
+      * @see Stroke#getOpacity()
+      * @see Graphic#getOpacity()
+      */
+     @Override
+     public Expression<R, ? extends Number> getOpacity() {
+         return defaultToOne(opacity);
+     }
+ 
+     /**
+      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * If this method is never invoked, then the default value is literal 1 (totally opaque).
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new level of translucency, or {@code null} for resetting the default value.
+      */
+     @Override
+     public void setOpacity(final Expression<R, ? extends Number> value) {
+         opacity = value;
+     }
+ 
+     /**
+      * Returns the selection of false-color channels for a multi-spectral raster source.
+      * Either red, green, and blue channels are selected, or a single grayscale channel is selected.
+      * Contrast enhancement may be applied to each channel in isolation.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this fill, and conversely.</p>
+      *
+      * @return the selection of channels.
+      */
+     public Optional<ChannelSelection<R>> getChannelSelection() {
+         return Optional.ofNullable(channelSelection);
+     }
+ 
+     /**
+      * Sets the selection of false-color channels for a multi-spectral raster source.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new selection of channels, or {@code null} for none.
+      */
+     public void setChannelSelection(final ChannelSelection<R> value) {
+         channelSelection = value;
+     }
+ 
+     /**
+      * Returns the behavior when multiple raster images in a layer overlap each other.
+      *
+      * @return behavior when multiple raster images in a layer overlap each other.
+      */
+     public OverlapBehavior getOverlapBehavior() {
+         final var value = overlapBehavior;
+         return (value != null) ? value : OverlapBehavior.LATEST_ON_TOP;
+         // Default value is unspecified, we use LATEST_ON_TOP for now.
+     }
+ 
+     /**
+      * Set the behavior when multiple raster images in a layer overlap each other.
+      *
+      * @param  value  new behavior, or {@code null} for resetting the default value.
+      */
+     public void setOverlapBehavior(final OverlapBehavior value) {
+         overlapBehavior = value;
+     }
+ 
+     /**
+      * Returns the mapping of fixed-numeric pixel values to colors.
+      * It can be used for defining the olors of a palette-type raster source.
+      * For example, a DEM raster giving elevations in meters above sea level
+      * can be translated to a colored image.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this fill, and conversely.</p>
+      *
+      * @return color map for the raster.
+      */
+     public Optional<ColorMap<R>> getColorMap() {
+         return Optional.ofNullable(colorMap);
+     }
+ 
+     /**
+      * Sets the mapping of fixed-numeric pixel values to colors.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new color map for the raster, or {@code null} if none.
+      */
+     public void setColorMap(final ColorMap<R> value) {
+         colorMap = value;
+     }
+ 
+     /**
+      * Returns the contrast enhancement for the whole image.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.
+      *
+      * @return contrast enhancement for the whole image.
+      *
+      * @see SelectedChannel#getContrastEnhancement()
+      */
+     public Optional<ContrastEnhancement<R>> getContrastEnhancement() {
+         return Optional.ofNullable(contrastEnhancement);
+     }
+ 
+     /**
+      * Sets the contrast enhancement applied to the whole image.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new contrast enhancement, or {@code null} if none.
+      *
+      * @see SelectedChannel#setContrastEnhancement(ContrastEnhancement)
+      */
+     public void setContrastEnhancement(final ContrastEnhancement<R> value) {
+         contrastEnhancement = value;
+     }
+ 
+     /**
+      * Returns the relief shading to apply to the image for a three-dimensional visual effect.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.
+      *
+      * @return the relief shading to apply.
+      */
+     public Optional<ShadedRelief<R>> getShadedRelief() {
+         return Optional.ofNullable(shadedRelief);
+     }
+ 
+     /**
+      * Sets the relief shading to apply to the image for a three-dimensional visual effect.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new relief shading to apply, or {@code null} if none.
+      */
+     public void setShadedRelief(final ShadedRelief<R> value) {
+         shadedRelief = value;
+     }
+ 
+     /**
+      * How to outline individual source rasters in a multi-raster set.
+      * The value should be either a {@link LineSymbolizer} or {@link PolygonSymbolizer}.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return Line or polygon symbolizer to use for outlining source rasters.
+      */
+     public Optional<Symbolizer<R>> getImageOutline() {
+         return Optional.ofNullable(imageOutline);
+     }
+ 
+     /**
+      * Sets how to outline individual source rasters in a multi-raster set.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new line or polygon symbolizer to use, or {@code null} if none.
+      */
+     public void setImageOutline(final Symbolizer<R> value) {
+         imageOutline = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {opacity, channelSelection, overlapBehavior,
+                 colorMap, contrastEnhancement, shadedRelief, imageOutline};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public RasterSymbolizer<R> clone() {
+         final var clone = (RasterSymbolizer<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (channelSelection    != null) channelSelection    = channelSelection.clone();
+         if (colorMap            != null) colorMap            = colorMap.clone();
+         if (contrastEnhancement != null) contrastEnhancement = contrastEnhancement.clone();
+         if (shadedRelief        != null) shadedRelief        = shadedRelief.clone();
+         if (imageOutline        != null) imageOutline        = imageOutline.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
index 0000000000,36055b9671..e90163ae6b
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Rule.java
@@@ -1,0 -1,513 +1,513 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlElementRef;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ import org.opengis.metadata.citation.OnlineResource;
+ import org.apache.sis.internal.jaxb.Context;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.ArgumentChecks;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Filter;
++import org.apache.sis.filter.Filter;
+ 
+ 
+ /**
+  * Rendering instructions grouped by feature-property conditions and map scales.
+  * A rule consists of two important parts: a {@linkplain Filter filter} and a list of symbols.
+  * When drawing a given feature, the rendering engine examines each rule in the style,
+  * first checking its filter. If the feature is accepted by the filter,
+  * then all {@link Symbolizer} for that rule are applied to the given feature.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "RuleType", propOrder = {
+     "name",
+     "description",
+     "legend",
+ //  "filter",           // XML encoding not yet available.
+     "elseFilter",
+     "minScale",
+     "maxScale",
+     "symbolizers"
+ })
+ @XmlRootElement(name = "Rule")
+ public class Rule<R> extends StyleElement<R> {
+     /**
+      * Name for this rule, or {@code null} if none.
+      *
+      * @see #getName()
+      * @see #setName(String)
+      */
+     @XmlElement(name = "Name")
+     protected String name;
+ 
+     /**
+      * Information for user interfaces, or {@code null} if none.
+      *
+      * @see #getDescription()
+      * @see #setDescription(Description)
+      */
+     @XmlElement(name = "Description")
+     protected Description<R> description;
+ 
+     /**
+      * Small graphic to draw in a legend window, or {@code null} if none.
+      *
+      * @see #getLegend()
+      * @see #setLegend(LegendGraphic)
+      */
+     @XmlElement(name = "LegendGraphic")
+     protected LegendGraphic<R> legend;
+ 
+     /**
+      * Filter that will limit the features, or {@code null} if none.
+      *
+      * @see #getFilter()
+      * @see #setFilter(Filter)
+      */
+ //  @XmlElement(name = "Filter", namespace = "http://www.opengis.net/ogc")
+     protected Filter<R> filter;
+ 
+     /**
+      * Whether this {@code Rule} will be applied only if no other rules in the containing style apply.
+      *
+      * @see #isElseFilter()
+      * @see #setElseFilter(boolean)
+      */
+     protected boolean isElseFilter;
+ 
+     /**
+      * Minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+      *
+      * @see #getMinScaleDenominator()
+      * @see #setMaxScaleDenominator(double)
+      */
+     protected double minScale;
+ 
+     /**
+      * Maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+      *
+      * @see #getMaxScaleDenominator()
+      * @see #setMaxScaleDenominator(double)
+      */
+     protected double maxScale;
+ 
+     /**
+      * Description of how a feature is to appear on a map.
+      *
+      * @see #symbolizers()
+      */
+     @XmlElementRef(name = "Symbolizer")
+     private List<Symbolizer<R>> symbolizers;
+ 
+     /**
+      * If the style comes from an external XML file, the original source. Otherwise {@code null}.
+      *
+      * @see #getOnlineResource()
+      * @see #setOnlineSource(OnlineResource)
+      */
+     protected OnlineResource onlineSource;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Rule() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an initially empty rule.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Rule(final StyleFactory<R> factory) {
+         super(factory);
+         maxScale = Double.POSITIVE_INFINITY;
+         symbolizers = new ArrayList<>();
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Rule(final Rule<R> source) {
+         super(source);
+         name         = source.name;
+         description  = source.description;
+         legend       = source.legend;
+         filter       = source.filter;
+         minScale     = source.minScale;
+         maxScale     = source.maxScale;
+         onlineSource = source.onlineSource;
+         symbolizers  = new ArrayList<>(source.symbolizers);
+     }
+ 
+     /**
+      * Returns the name for this rule.
+      * This can be any string that uniquely identifies this rule within a given canvas.
+      * It is not meant to be human-friendly. For a human-friendly label,
+      * see the {@linkplain Description#getTitle() title} instead.
+      *
+      * @return a name for this rule.
+      */
+     public Optional<String> getName() {
+         return Optional.ofNullable(name);
+     }
+ 
+     /**
+      * Sets a name for this rule.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new name for this rule, or {@code null} if none.
+      */
+     public void setName(final String value) {
+         name = value;
+     }
+ 
+     /**
+      * Returns the description of this rule.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this rule, and conversely.
+      *
+      * @return information for user interfaces.
+      */
+     public Optional<Description<R>> getDescription() {
+         return Optional.ofNullable(description);
+     }
+ 
+     /**
+      * Sets a description of this rule.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new information for user interfaces, or {@code null} if none.
+      */
+     public void setDescription(final Description<R> value) {
+         description = value;
+     }
+ 
+     /**
+      * Returns a small graphic that could be used by the rendering engine to draw a legend window.
+      * User interfaces may present the user with a legend that indicates how features of a given type are being portrayed.
+      * Through its {@code LegendGraphic} property, a {@code Rule} can provide a custom picture to be used in such a legend window.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this rule, and conversely.</p>
+      *
+      * @return small graphic to draw in a legend window.
+      */
+     public Optional<LegendGraphic<R>> getLegend() {
+         return Optional.ofNullable(legend);
+     }
+ 
+     /**
+      * Sets a small graphic that could be used by the rendering engine to draw a legend window.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new legend graphic, or {@code null} if none.
+      */
+     public void setLegend(final LegendGraphic<R> value) {
+         legend = value;
+     }
+ 
+     /**
+      * Returns the filter that will limit the features for which this rule will apply.
+      * This value should be used only if {@link #isElseFilter()} returns {@code false},
+      * in which case this rule applies to all features.
+      *
+      * @return the filter that will limit the features.
+      */
+     public Filter<R> getFilter() {
+         if (isElseFilter) {
+             return Filter.exclude();
+         }
+         final var value = filter;
+         return (value != null) ? value : Filter.include();
+     }
+ 
+     /**
+      * Sets the filter that will limit the features for which this rule will apply.
+      * If this method is never invoked, then the default value is {@link Filter#include()}.
+      * Invoking this method forces {@link #isElseFilter()} to {@code false}.
+      *
+      * @param  value  new filter that will limit the features, or {@code null} if none.
+      */
+     public void setFilter(final Filter<R> value) {
+         isElseFilter = false;
+         filter = value;
+     }
+ 
+     /**
+      * Returns true if this {@code Rule} will be applied only if no other rules in the containing style apply.
+      * If this is true, then the {@linkplain #getFilter() filter} should be ignored.
+      *
+      * <p>The "Else Filter" is implicitly a filter with a condition that depends on the enclosing style.
+      * Consequently, it can not be expressed as a standalone {@code Filter} expression in this rule.</p>
+      *
+      * @return true if the filter is an else filter.
+      */
+     public boolean isElseFilter() {
+         return isElseFilter;
+     }
+ 
+     /**
+      * Sets or unset this filter to an else filter.
+      *
+      * @param  value  whether the filter is the "else" filter.
+      */
+     public void setElseFilter(final boolean value) {
+         isElseFilter = value;
+     }
+ 
+     /**
+      * Invoked by JAXB at marshalling time for expressing the boolean {@code isElseFilter} value as an XML element.
+      */
+     @XmlElement(name = "ElseFilter")
+     private ElseFilter getElseFilter() {
+         return isElseFilter ? ElseFilter.INSTANCE : null;
+     }
+ 
+     /**
+      * Invoked at JAXB unmarshalling time when an {@code <ElseFilter/>} element is found.
+      */
+     private void setElseFilter(final ElseFilter value) {
+         isElseFilter = (value != null);
+     }
+ 
+     /**
+      * Returns the minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+      * If, for example, this value was 10000, then this rule would only apply at scales of 1:<var>X</var>
+      * where <var>X</var> is greater than 10000. A value of zero indicates that there is no minimum.
+      *
+      * <h4>Relationship with real world lengths</h4>
+      * The values used are scale denominators relative to a “standardized rendering pixel size”.
+      * That size is defined as a square with sides of 0.28 millimeters. If the real pixel size
+      * is different or if the CRS uses angular units instead than linear, then the renderer shall
+      * take those information in account as described in OGC 05-077r4 §10.2.
+      *
+      * @return minimum scale value, inclusive.
+      */
+     public double getMinScaleDenominator() {
+         return minScale;
+     }
+ 
+     /**
+      * Invoked by JAXB for marshalling the minimum scale denominator.
+      * If both the minimum and maximum are the default values, then this property is omitted.
+      * If a maximum value exists, then the zero minimum is explicitly written for clarity.
+      */
+     @XmlElement(name = "MinScaleDenominator")
+     private Double getMinScale() {
+         final var value = minScale;
+         return (value > 0 || maxScale != Double.POSITIVE_INFINITY) ? value : null;
+     }
+ 
+     /**
+      * Sets the minimum value (inclusive) in the denominator of map scale at which this rule will apply.
+      * If the given value is greater than the maximum scale, then that maximum is discarded.
+      * If this method is never invoked, then the default value is 0.
+      *
+      * @param  value  new minimum scale value (inclusive).
+      */
+     public void setMinScaleDenominator(final double value) {
+         ArgumentChecks.ensurePositive("MinScaleDenominator", value);
+         minScale = value;
+         if (value > maxScale) {
+             maxScale = Double.POSITIVE_INFINITY;
+         }
+     }
+ 
+     /**
+      * Invoked by JAXB for unmarshalling the minimum scale denominator.
+      * The argument validity check assumes that this method is invoked only once.
+      * If this assumption is violated, the check allows range restriction but not expansion.
+      * If the given value is invalid, a warning is reported but the unmarshalling continue.
+      */
+     private void setMinScale(final Double value) {
+         if (isValidScale("MinScaleDenominator", value)) {
+             minScale = value;
+         }
+     }
+ 
+     /**
+      * Returns the maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+      * If, for example, this value was 10000, then this rule would only apply at scales of 1:<var>X</var>
+      * where <var>X</var> is less than 10000.
+      * An {@linkplain Double#POSITIVE_INFINITY infinite} value indicates that there is no maximum.
+      *
+      * <h4>Relationship with real world lengths</h4>
+      * The same discussion than {@link #getMinScaleDenominator()} applies also to the maximum scale value.
+      *
+      * @return maximum scale value, exclusive.
+      */
+     public double getMaxScaleDenominator() {
+         return maxScale;
+     }
+ 
+     /**
+      * Invoked by JAXB for marshalling the maximum scale denominator.
+      * If the value is positive infinity, then the property is omitted.
+      */
+     @XmlElement(name = "MaxScaleDenominator")
+     private Double getMaxScale() {
+         final var value = maxScale;
+         return (value != Double.POSITIVE_INFINITY) ? value : null;
+     }
+ 
+     /**
+      * Sets the maximum value (exclusive) in the denominator of map scale at which this rule will apply.
+      * If the given value is less than the minimum scale, then that minimum is discarded.
+      * If this method is never invoked, then the default value is {@link Double#POSITIVE_INFINITY}.
+      *
+      * @param  value  new maximum scale value (exclusive).
+      */
+     public void setMaxScaleDenominator(final double value) {
+         ArgumentChecks.ensureStrictlyPositive("MaxScaleDenominator", value);
+         maxScale = value;
+         if (value < minScale) {
+             minScale = 0;
+         }
+     }
+ 
+     /**
+      * Invoked by JAXB for unmarshalling the maximum scale denominator.
+      * The argument validity check assumes that this method is invoked only once.
+      * If this assumption is violated, the check allows range restriction but not expansion.
+      * If the given value is invalid, a warning is reported but the unmarshalling continue.
+      */
+     private void setMaxScale(final Double value) {
+         if (isValidScale("MaxScaleDenominator", value)) {
+             maxScale = value;
+         }
+     }
+ 
+     /**
+      * Indicates whether an unmarshalled minimum or maximum scale denominator is inside the expected range of values.
+      * If the given value is invalid, a warning is emitted to the JAXB unmarshaller and the caller will keep the default value.
+      */
+     private boolean isValidScale(final String name, final Double value) {
+         boolean isValidScale = (value != null);
+         if (isValidScale) {
+             isValidScale = (value >= minScale && value <= maxScale);
+             if (!isValidScale) {
+                 Context.warningOccured(Context.current(), Rule.class, "set".concat(name), Errors.class,
+                                        Errors.Keys.ValueOutOfRange_4, name, minScale, maxScale, value);
+             }
+         }
+         return isValidScale;
+     }
+ 
+     /**
+      * Returns the description of how a feature is to appear on a map.
+      * Each symbolizer describes how the shape should appear,
+      * together with graphical properties such as color and opacity.
+      * The predefined type of symbolizers are
+      * {@linkplain LineSymbolizer line},
+      * {@linkplain PolygonSymbolizer polygon},
+      * {@linkplain PointSymbolizer point},
+      * {@linkplain TextSymbolizer text}, and
+      * {@linkplain RasterSymbolizer raster} symbolizers.
+      *
+      * <p>The returned collection is <em>live</em>:
+      * changes in that collection are reflected into this object, and conversely.</p>
+      *
+      * @return the list of symbolizers, as a live collection.
+      */
+     @SuppressWarnings("ReturnOfCollectionOrArrayField")
+     public List<Symbolizer<R>> symbolizers() {
+         return symbolizers;
+     }
+ 
+     /**
+      * If the style comes from an external XML file, the original source.
+      * This property may be non-null if a XML document specified this rule
+      * by a link to another XML document.
+      *
+      * @return the original source of this rule.
+      */
+     public Optional<OnlineResource> getOnlineSource() {
+         return Optional.ofNullable(onlineSource);
+     }
+ 
+     /**
+      * If the style comes from an external XML file, the original source.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * <h4>Effect on XML marshalling</h4>
+      * Setting this property to a non-null value has the following effect:
+      * When this rule is written in a XML document, then instead of writing
+      * the XML elements describing this rule,
+      * the specified link will be written instead.
+      *
+      * @todo Above-describing marshalling is not yet implemented.
+      *
+      * @param  value  new source of this rule, or {@code null} if none.
+      */
+     public void setOnlineSource(final OnlineResource value) {
+         onlineSource = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {name, description, legend, filter, minScale, maxScale, symbolizers, onlineSource};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Rule<R> clone() {
+         final var clone = (Rule<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (description != null) description = description.clone();
+         if (legend      != null) legend      = legend.clone();
+         symbolizers = new ArrayList<>(symbolizers);
+         symbolizers.replaceAll(Symbolizer::clone);
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
index 0000000000,48298dd9cb..cb946149bb
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/SelectedChannel.java
@@@ -1,0 -1,173 +1,173 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Information about a channel to use in a multi-spectral source.
+  * Channels are identified by data-dependent character identifiers.
+  * Commonly, channels will be labelled as "1", "2", <i>etc</i>.
+  * A set of selected channels is contained in {@link ChannelSelection}.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Ian Turton (CCG)
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "SelectedChannelType", propOrder = {
+     "sourceChannelName",
+     "contrastEnhancement"
+ })
+ // No root element is specified in OGC 05-077r4.
+ public class SelectedChannel<R> extends StyleElement<R> {
+     /**
+      * The channel's name, or {@code null} if unspecified.
+      *
+      * @see #getSourceChannelName()
+      * @see #setSourceChannelName(Expression)
+      *
+      * @todo Needs an adapter from expression to plain string.
+      */
+     @XmlElement(name = "SourceChannelName", required = true)
+     protected Expression<R,String> sourceChannelName;
+ 
+     /**
+      * Contrast enhancement applied to the selected channel in isolation, or {@code null} if none.
+      *
+      * @see #getContrastEnhancement()
+      * @see #setContrastEnhancement(ContrastEnhancement)
+      */
+     @XmlElement(name = "ContrastEnhancement")
+     protected ContrastEnhancement<R> contrastEnhancement;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private SelectedChannel() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates an initially empty selected channel.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public SelectedChannel(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public SelectedChannel(final SelectedChannel<R> source) {
+         super(source);
+         sourceChannelName   = source.sourceChannelName;
+         contrastEnhancement = source.contrastEnhancement;
+     }
+ 
+     /**
+      * Returns the channel's name.
+      *
+      * @return the channel's name, or {@code null} if unspecified.
+      *
+      * @todo Shall never be {@code null}. We need to think about some default value.
+      */
+     public Expression<R,String> getSourceChannelName() {
+         return sourceChannelName;
+     }
+ 
+     /**
+      * Sets the channel's name.
+      *
+      * @param  value  the channel's name, or {@code null} if unspecified.
+      */
+     public void setSourceChannelName(final Expression<R,String> value) {
+         sourceChannelName = value;
+     }
+ 
+     /**
+      * Returns the contrast enhancement applied to the selected channel in isolation.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.
+      *
+      * @return contrast enhancement for the selected channel.
+      *
+      * @see RasterSymbolizer#getContrastEnhancement()
+      */
+     public Optional<ContrastEnhancement<R>> getContrastEnhancement() {
+         return Optional.ofNullable(contrastEnhancement);
+     }
+ 
+     /**
+      * Sets the contrast enhancement applied to the selected channel in isolation.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new contrast enhancement, or {@code null} if none.
+      *
+      * @see RasterSymbolizer#setContrastEnhancement(ContrastEnhancement)
+      */
+     public void setContrastEnhancement(final ContrastEnhancement<R> value) {
+         contrastEnhancement = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {sourceChannelName, contrastEnhancement};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public SelectedChannel<R> clone() {
+         final var clone = (SelectedChannel<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (contrastEnhancement != null) {
+             contrastEnhancement = contrastEnhancement.clone();
+         }
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
index 0000000000,a01dea558c..82e171e0fd
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/ShadedRelief.java
@@@ -1,0 -1,155 +1,155 @@@
+ /*
+  * 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.style.se1;
+ 
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Relief shading (or “hill shading”) applied to an image for a three-dimensional visual effect.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Ian Turton (CCG)
+  * @author  Johann Sorel (Geomatys)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "ShadedReliefType", propOrder = {
+     "brightnessOnly",
+     "reliefFactor"
+ })
+ @XmlRootElement(name = "ShadedRelief")
+ public class ShadedRelief<R> extends StyleElement<R> {
+     /**
+      * Whether to apply the shading to the image generated so far by other layers.
+      *
+      * @see #isBrightnessOnly()
+      * @see #setBrightnessOnly(Expression)
+      *
+      * @todo Needs an adapter from expression to plain boolean.
+      */
+     @XmlElement(name = "BrightnessOnly")
+     protected Expression<R,Boolean> brightnessOnly;
+ 
+     /**
+      * Amount of exaggeration to use for the height of the hills, or {@code null} for the default value.
+      *
+      * @see #getReliefFactor()
+      * @see #setReliefFactor(Expression)
+      */
+     @XmlElement(name = "ReliefFactor")
+     protected Expression<R, ? extends Number> reliefFactor;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private ShadedRelief() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a shaded relief initialized to implementation-specific default values.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public ShadedRelief(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public ShadedRelief(final ShadedRelief<R> source) {
+         super(source);
+         brightnessOnly = source.brightnessOnly;
+         reliefFactor   = source.reliefFactor;
+     }
+ 
+     /**
+      * Returns whether to apply the shading to the image generated so far by other layers.
+      * If {@code false}, then the shading is applied only on the layer being rendered by
+      * the current {@link RasterSymbolizer}.
+      *
+      * @return whether to apply the shading to the image generated so far by other layers.
+      */
+     public Expression<R,Boolean> isBrightnessOnly() {
+         return defaultToFalse(brightnessOnly);
+     }
+ 
+     /**
+      * Sets whether to apply the shading to the image generated so far by other layers.
+      * If this method is never invoked, then the default value is literal false.
+      *
+      * @param  value  new policy, or {@code null} for resetting the default value.
+      */
+     public void setBrightnessOnly(final Expression<R,Boolean> value) {
+         brightnessOnly = value;
+     }
+ 
+     /**
+      * Returns the amount of exaggeration to use for the height of the hills.
+      * A value of around 55 gives reasonable results for Earth-based DEMs.
+      *
+      * @return amount of exaggeration to use for the height of the hills.
+      */
+     public Expression<R, ? extends Number> getReliefFactor() {
+         final var value = reliefFactor;
+         return (value != null) ? value : factory.relief;
+     }
+ 
+     /**
+      * Sets the amount of exaggeration to use for the height of the hills.
+      * If this method is never invoked, then the default value is implementation-specific.
+      *
+      * @param  value  new amount of exaggeration, or {@code null} for resetting the default value.
+      */
+     public void setReliefFactor(final Expression<R, ? extends Number> value) {
+         reliefFactor = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {brightnessOnly, reliefFactor};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public ShadedRelief<R> clone() {
+         return (ShadedRelief<R>) super.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
index 0000000000,70f52769f3..431741425b
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Stroke.java
@@@ -1,0 -1,492 +1,492 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to draw styled lines.
+  * Stroke objects are contained by {@link LineSymbolizer} and {@link PolygonSymbolizer}.
+  * There are three basic types of strokes: solid-color, {@link GraphicFill} (stipple),
+  * and repeated linear {@link GraphicStroke}.
+  * A repeated linear graphic is plotted linearly and has its graphic symbol bent around the curves
+  * of the line string, and a graphic fill has the pixels of the line rendered with a repeating area-fill pattern.
+  * If neither a {@linkplain #getGraphicFill() graphic fill} nor {@linkplain #getGraphicStroke() graphic stroke}
+  * element is given, then the line symbolizer will render a solid color.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "StrokeType", propOrder = {
+     "graphicFill",
+     "graphicStroke",
+ //  "svgParameter"
+ })
+ @XmlRootElement(name = "Stroke")
+ public class Stroke<R> extends StyleElement<R> implements Translucent<R> {
+     /**
+      * Graphic for tiling the (thin) area of the line, or {@code null} if none.
+      * This property and {@link #graphicStroke} are mutually exclusive.
+      *
+      * @see #getGraphicFill()
+      * @see #setGraphicFill(GraphicFill)
+      */
+     @XmlElement(name = "GraphicFill")
+     protected GraphicFill<R> graphicFill;
+ 
+     /**
+      * Graphic to repeat along the path of the lines, or {@code null} if none.
+      * This property and {@link #graphicFill} are mutually exclusive.
+      *
+      * @see #getGraphicStroke()
+      * @see #setGraphicStroke(GraphicStroke)
+      */
+     @XmlElement(name = "GraphicStroke")
+     protected GraphicStroke<R> graphicStroke;
+ 
+     /**
+      * Color of the line if it is to be solid-color filled, or {@code null} for the default value.
+      * The default value specified by OGC 05-077r4 standard is black.
+      *
+      * <p>This property is used when both {@link #graphicFill} and {@link #graphicStroke} are null.
+      * In XML documents, this is encoded inside a {@code <SvgParameter name="stroke">} element.</p>
+      *
+      * @see #getColor()
+      * @see #setColor(Expression)
+      */
+     protected Expression<R,Color> color;
+ 
+     /**
+      * Level of translucency as a floating point number between 0 and 1 (inclusive), or {@code null} the default value.
+      * The default value specified by OGC 05-077r4 standard is 1.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-opacity">} element.</p>
+      *
+      * @see #getOpacity()
+      * @see #setOpacity(Expression)
+      */
+     protected Expression<R, ? extends Number> opacity;
+ 
+     /**
+      * Absolute width of the line stroke as a positive floating point number, or {@code null} for the default value.
+      * The default value specified by OGC 05-077r4 standard is 1.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-width">} element.</p>
+      *
+      * @see #getWidth()
+      * @see #setWidth(Expression)
+      */
+     protected Expression<R, ? extends Number> width;
+ 
+     /**
+      * How the various segments of a (thick) line string should be joined, or {@code null} the default value.
+      * The default value is implementation-specific.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-linejoin">} element.</p>
+      *
+      * @see #getLineJoin()
+      * @see #setLineJoin(Expression)
+      */
+     protected Expression<R,String> lineJoin;
+ 
+     /**
+      * How the beginning and ending segments of a line string will be terminated, or {@code null} the default value.
+      * The default value is implementation-specific.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-linecap">} element.</p>
+      *
+      * @see #getLineCap()
+      * @see #setLineCap(Expression)
+      */
+     protected Expression<R,String> lineCap;
+ 
+     /**
+      * Dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-dasharray">} element.</p>
+      *
+      * @see #getDashArray()
+      * @see #setDashArray(Expression)
+      */
+     protected Expression<R,float[]> dashArray;
+ 
+     /**
+      * Distance offset into the dash array to begin drawing, or {@code null} for the default value.
+      * The default value is zero.
+      *
+      * <p>In XML documents, this is encoded inside a {@code <SvgParameter name="stroke-dashoffset">} element.</p>
+      *
+      * @see #getDashOffset()
+      * @see #setDashOffset(Expression)
+      */
+     protected Expression<R,Integer> dashOffset;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private Stroke() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Stroke(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public Stroke(final Stroke<R> source) {
+         super(source);
+         graphicFill   = source.graphicFill;
+         graphicStroke = source.graphicStroke;
+         color         = source.color;
+         opacity       = source.opacity;
+         width         = source.width;
+         lineJoin      = source.lineJoin;
+         lineCap       = source.lineCap;
+         dashArray     = source.dashArray;
+         dashOffset    = source.dashOffset;
+     }
+ 
+     /**
+      * Indicates that line should be drawn by tiling the (thin) area of the line with the given graphic.
+      * Between {@code getGraphicFill()} and {@link #getGraphicStroke()}, only one may return a non-null value
+      * because a {@code Stroke} can have a {@code GraphicFill} or a {@code GraphicStroke}, but not both.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return graphic for tiling the (thin) area of the line.
+      *
+      * @see Fill#getGraphicFill()
+      */
+     public Optional<GraphicFill<R>> getGraphicFill() {
+         return Optional.ofNullable(graphicFill);
+     }
+ 
+     /**
+      * Specifies that line should be drawn by tiling the (thin) area of the line with the given graphic.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * <p>Setting a non-null value causes {@link #getGraphicStroke()} to
+      * return {@code null} because those two properties are mutually exclusive.</p>
+      *
+      * @param  value  new graphic for tiling the (thin) area of the line, or {@code null} if none.
+      *
+      * @see Fill#setGraphicFill(GraphicFill)
+      */
+     public void setGraphicFill(final GraphicFill<R> value) {
+         graphicFill = value;
+         if (value != null) {
+             graphicStroke = null;
+         }
+     }
+ 
+     /**
+      * Indicates that lines should be drawn by repeatedly plotting the given graphic.
+      * The graphic is repeated along the path of the lines, rotating it according to the orientation of the line.
+      * Between {@link #getGraphicFill()} and {@code getGraphicStroke()}, only one may return a non-null value
+      * because a {@code Stroke} can have a {@link GraphicFill} or a {@link GraphicStroke}, but not both.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this stroke, and conversely.</p>
+      *
+      * @return graphic to repeat along the path of the lines.
+      */
+     public Optional<GraphicStroke<R>> getGraphicStroke() {
+         return Optional.ofNullable(graphicStroke);
+     }
+ 
+     /**
+      * Specifies that lines should be drawn by repeatedly plotting the given graphic.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * <p>Setting a non-null value causes {@link #getGraphicFill()} to
+      * return {@code null} because those two properties are mutually exclusive.</p>
+      *
+      * @param  value  new graphic to repeat along the path of the lines, or {@code null} if none.
+      */
+     public void setGraphicStroke(final GraphicStroke<R> value) {
+         graphicStroke = value;
+         if (value != null) {
+             graphicFill = null;
+         }
+     }
+ 
+     /**
+      * Indicates the color of the line if it is to be solid-color filled.
+      * This is used when both {@linkplain #getGraphicFill() graphic fill}
+      * and {@linkplain #getGraphicStroke() graphic stroke} are null.
+      *
+      * @return color of the line if it is to be solid-color filled.
+      *
+      * @see Fill#getColor()
+      */
+     public Expression<R,Color> getColor() {
+         final var value = color;
+         return (value != null) ? value : factory.black;
+     }
+ 
+     /**
+      * Sets the color of the line if it is to be solid-color filled.
+      * If this method is never invoked, then the default value is {@link Fill#BLACK}.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * <p>Setting a non-null value clears the {@linkplain #getGraphicFill() graphic fill} and the
+      * {@linkplain #getGraphicStroke() graphic stroke} because those three properties are mutually exclusive.</p>
+      *
+      * @param  value  color of the line if solid-color filled, or {@code null} for resetting the default value.
+      *
+      * @see Fill#setColor(Expression)
+      */
+     public void setColor(final Expression<R,Color> value) {
+         color = value;
+         if (value != null) {
+             graphicFill   = null;
+             graphicStroke = null;
+         }
+     }
+ 
+     /**
+      * Sets the color and opacity together.
+      * The opacity is derived from the alpha value of the given color.
+      *
+      * @param  value  new color and opacity, or {@code null} for resetting the defaults.
+      */
+     public void setColorAndOpacity(Color value) {
+         if (value  == null) {
+             color   = null;
+             opacity = null;
+         } else {
+             if ((opacity = opacity(value)) != null) {
+                 value = new Color(value.getRGB() | 0xFF000000);
+             }
+             color = literal(value);
+         }
+     }
+ 
+     /**
+      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+      *
+      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+      *
+      * @see Fill#getOpacity()
+      * @see Graphic#getOpacity()
+      * @see RasterSymbolizer#getOpacity()
+      */
+     @Override
+     public Expression<R, ? extends Number> getOpacity() {
+         return defaultToOne(opacity);
+     }
+ 
+     /**
+      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * If this method is never invoked, then the default value is literal 1 (totally opaque).
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new level of translucency, or {@code null} for resetting the default value.
+      */
+     @Override
+     public void setOpacity(final Expression<R, ? extends Number> value) {
+         opacity = value;
+     }
+ 
+     /**
+      * Gives the absolute width of the line stroke as a floating point number.
+      * The unit of measurement is given by {@link Symbolizer#getUnitOfMeasure()}.
+      * Fractional numbers are allowed, but negative numbers are not.
+      *
+      * @return absolute width of the line stroke as a positive floating point number.
+      */
+     public Expression<R, ? extends Number> getWidth() {
+         return defaultToOne(width);
+     }
+ 
+     /**
+      * Sets the absolute width of the line stroke as a floating point number.
+      * If this method is never invoked, then the default value 1.0.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new width of the line stroke, or {@code null} for resetting the default value.
+      */
+     public void setWidth(final Expression<R, ? extends Number> value) {
+         width = value;
+     }
+ 
+     /**
+      * Indicates how the various segments of a (thick) line string should be joined.
+      * Valid values are "miter", "round", and "bevel".
+      *
+      * @return how segments of a (thick) line string should be joined.
+      */
+     public Expression<R,String> getLineJoin() {
+         final var value = lineJoin;
+         return (value != null) ? value : factory.bevel;
+     }
+ 
+     /**
+      * Sets how the various segments of a (thick) line string should be joined.
+      * If this method is never invoked, then the default value is literal "bevel".
+      * That default value is implementation-specific.
+      *
+      * @param  value  how segments of a line string should be joined, or {@code null} for resetting the default value.
+      */
+     public void setLineJoin(final Expression<R,String> value) {
+         lineJoin = value;
+     }
+ 
+     /**
+      * Indicates how the beginning and ending segments of a line string will be terminated.
+      * Valid values are "butt", "round", and "square".
+      *
+      * @return how the beginning and ending segments of a line string will be terminated.
+      */
+     public Expression<R,String> getLineCap() {
+         final var value = lineCap;
+         return (value != null) ? value : factory.square;
+     }
+ 
+     /**
+      * Sets how the beginning and ending segments of a line string will be terminated.
+      * If this method is never invoked, then the default value is literal "square".
+      * That default value is implementation-specific.
+      *
+      * @param  value  how a line string should be terminated, or {@code null} for resetting the default value.
+      */
+     public void setLineCap(final Expression<R,String> value) {
+         lineCap = value;
+     }
+ 
+     /**
+      * Indicates the dash pattern as a space-separated sequence of floating point numbers.
+      * The first number represents the length of the first dash to draw.
+      * The second number represents the length of space to leave.
+      * This continues to the end of the list then repeats.
+      * If {@code null}, then lines will be drawn as solid and unbroken.
+      *
+      * @return dash pattern as a space-separated sequence of numbers, or empty for a solid line.
+      */
+     public Optional<Expression<R,float[]>> getDashArray() {
+         return Optional.ofNullable(dashArray);
+     }
+ 
+     /**
+      * Sets the dash pattern as a space-separated sequence of floating point numbers.
+      * If this method is never invoked, then the default value is {@code null} (solid line).
+      *
+      * @param  value  new dash pattern as a space-separated sequence of numbers, or {@code null} for a solid line.
+      */
+     public void setDashArray(final Expression<R,float[]> value) {
+         dashArray = value;
+     }
+ 
+     /**
+      * Indicates the distance offset into the dash array to begin drawing.
+      *
+      * @return distance offset into the dash array to begin drawing.
+      */
+     public Expression<R,Integer> getDashOffset() {
+         final var value = dashOffset;
+         return (value != null) ? value : factory.zeroAsInt;
+     }
+ 
+     /**
+      * Sets the distance offset into the dash array to begin drawing.
+      * If this method is never invoked, then the default value is 0.
+      *
+      * @param  value  new distance offset into the dash array, or {@code null} for resetting the default value.
+      */
+     public void setDashOffset(final Expression<R,Integer> value) {
+         dashOffset = value;
+     }
+ 
+     /*
+      * TODO: we need a private method like below for formatting above SVG parameters:
+      *
+      *     @XmlElement(name = "SvgParameter")
+      *     private List<SvgParameter> svgParameters();
+      *
+      * Where:
+      *
+      *     class SvgParameter {
+      *         @XmlAttribute(required = true)
+      *         private String name;
+      *
+      *         @XmlMixed
+      *         @XmlElementRef(name = "expression", namespace = "http://www.opengis.net/ogc")
+      *         private List<Expression<?,?>> content;
+      *     }
+      *
+      * See 05-077r4 §11.1.3.
+      */
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {graphicFill, graphicStroke, color, opacity, width, lineJoin, lineCap, dashArray, dashOffset};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Stroke<R> clone() {
+         final var clone = (Stroke<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (graphicFill   != null) graphicFill   = graphicFill.clone();
+         if (graphicStroke != null) graphicStroke = graphicStroke.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
index 0000000000,55df55df04..bb245cacac
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleElement.java
@@@ -1,0 -1,220 +1,219 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import java.util.Arrays;
+ import jakarta.xml.bind.annotation.XmlTransient;
+ import org.opengis.util.InternationalString;
+ import org.apache.sis.util.ArgumentChecks;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Literal;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Base class of all style objects.
+  * This base class can not be extended directly.
+  * Instead, one of the subclasses can be extended.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlTransient
+ public abstract class StyleElement<R> implements Cloneable {
+     /**
+      * The factory to use for creating expressions and child elements.
+      * This is typically the same factory than the one used for creating this element.
+      *
+      * @see FeatureTypeStyle#FACTORY
+      * @see CoverageStyle#FACTORY
+      */
+     protected final StyleFactory<R> factory;
+ 
+     /**
+      * Creates a new style element.
+      * Intentionally restricted to this package because {@link #properties()} is package-private.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     StyleElement(final StyleFactory<R> factory) {
+         ArgumentChecks.ensureNonNull("factory", factory);
+         this.factory = factory;
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     StyleElement(final StyleElement<R> source) {
+         factory = source.factory;
+     }
+ 
+     /**
+      * Creates a style element for XML unmarshalling.
+      * <em>This constructor is unsafe</em> and should be used only by JAXB reflection.
+      *
+      * @todo Allow the factory to be set according the parent {@link AbstractStyle} being unmarshalled.
+      *       We will need to use {@link ThreadLocal}.
+      */
+     StyleElement() {
+         factory = null;     // TODO
+     }
+ 
+     /**
+      * Returns a literal for the given value.
+      * This is a convenience method for use with setter methods that expect an expression.
+      *
+      * @param  <E>    type of value.
+      * @param  value  the value for which to return a literal, or {@code null} if none.
+      * @return literal for the given value, or {@code null} if the given value was null.
+      */
 -    public final <E> Literal<R,E> literal(final E value) {
++    public final <E> Expression<R,E> literal(final E value) {
+         return (value == null) ? null : factory.filterFactory.literal(value);
+     }
+ 
+     /**
+      * Returns the given expression if non-null, or literal {@code true} otherwise.
+      * This is a convenience method for the implementation of getter methods when
+      * a default value exists.
+      *
+      * @param  value  the value for which to apply a default value if {@code null}.
+      * @return the given value if non-null, or {@code true} literal otherwise.
+      */
+     protected final Expression<R,Boolean> defaultToTrue(final Expression<R,Boolean> value) {
+         return (value != null) ? value : factory.enabled;
+     }
+ 
+     /**
+      * Returns the given expression if non-null, or literal {@code false} otherwise.
+      * This is a convenience method for the implementation of getter methods when
+      * a default value exists.
+      *
+      * @param  value  the value for which to apply a default value if {@code null}.
+      * @return the given value if non-null, or a {@code false} literal otherwise.
+      */
+     protected final Expression<R,Boolean> defaultToFalse(final Expression<R,Boolean> value) {
+         return (value != null) ? value : factory.disabled;
+     }
+ 
+     /**
+      * Returns the given expression if non-null, or literal {@code 0.0} otherwise.
+      * This is a convenience method for the implementation of getter methods when
+      * a default value exists.
+      *
+      * @param  value  the value for which to apply a default value if {@code null}.
+      * @return the given value if non-null, or {@code 0.0} literal otherwise.
+      */
+     protected final Expression<R, ? extends Number> defaultToZero(final Expression<R, ? extends Number> value) {
+         return (value != null) ? value : factory.zero;
+     }
+ 
+     /**
+      * Returns the given expression if non-null, or literal {@code 0.5} otherwise.
+      * This is a convenience method for the implementation of getter methods when
+      * a default value exists.
+      *
+      * @param  value  the value for which to apply a default value if {@code null}.
+      * @return the given value if non-null, or {@code 0.5} literal otherwise.
+      */
+     protected final Expression<R, ? extends Number> defaultToHalf(final Expression<R, ? extends Number> value) {
+         return (value != null) ? value : factory.half;
+     }
+ 
+     /**
+      * Returns the given expression if non-null, or literal {@code 1.0} otherwise.
+      * This is a convenience method for the implementation of getter methods when
+      * a default value exists.
+      *
+      * @param  value  the value for which to apply a default value if {@code null}.
+      * @return the given value if non-null, or {@code 1.0} literal otherwise.
+      */
+     protected final Expression<R, ? extends Number> defaultToOne(final Expression<R, ? extends Number> value) {
+         return (value != null) ? value : factory.one;
+     }
+ 
+     /**
+      * Returns the opacity of the alpha value of the given color.
+      * If the color is totally opaque, then this method returns {@code null}.
+      *
+      * @param  color  color from which to get the opacity.
+      * @return opacity derived from the alpha value of the color, or {@code null} if totally opaque.
+      */
+     final Expression<R, ? extends Number> opacity(final Color color) {
+         final int alpha = color.getAlpha();
+         return (alpha != 255) ? literal(alpha / 256d) : null;
+         // Divide by 256 instead of 255 in order to get round numbers for alpha values 64, 128, etc.
+     }
+ 
+     /**
+      * Returns all properties contained in the subclasses.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      *
+      * @return all properties.
+      */
+     abstract Object[] properties();
+ 
+     /**
+      * Returns a hash code value for this object.
+      *
+      * @return a hash code value for this object.
+      */
+     @Override
+     public int hashCode() {
+         return getClass().hashCode() + Arrays.hashCode(properties());
+     }
+ 
+     /**
+      * Compares this element with the given object for equality.
+      *
+      * @param  obj  the other object to compare with this.
+      * @return whether the other object is equal to this.
+      */
+     @Override
+     public boolean equals(final Object obj) {
+         if (obj == this) {
+             return true;
+         }
+         return (obj != null) && (obj.getClass() == getClass()) &&
+                 Arrays.equals(properties(), ((StyleElement) obj).properties());
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      * ISO 19115 metadata and {@link InternationalString} members, if any,
+      * are not cloned neither in current Apache SIS version.
+      *
+      * @return a clone of this element.
+      */
+     @Override
+     @SuppressWarnings("unchecked")
+     public StyleElement<R> clone() {
+         try {
+             return (StyleElement<R>) super.clone();
+         } catch (CloneNotSupportedException e) {
+             throw new AssertionError(e);    // Should never happen since we are cloneable.
+         }
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
index 0000000000,51137d965e..777b150fc4
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/StyleFactory.java
@@@ -1,0 -1,534 +1,534 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.net.URI;
+ import java.awt.Color;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.internal.feature.AttributeConvention;
+ import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.ValueReference;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.filter.DefaultFilterFactory;
+ 
+ 
+ /**
+  * Factory of style elements.
+  * Style factory uses a {@link FilterFactory} instance that depends on the type of data to be styled.
+  * That type of data is specified by the parameterized type {@code <R>}.
 - * The two main types are {@link org.opengis.feature.Feature} and {@link org.apache.sis.coverage.BandedCoverage}.
++ * The two main types are {@link org.apache.sis.feature.AbstractFeature}
++ * and {@link org.apache.sis.coverage.BandedCoverage}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since   1.5
+  */
+ public class StyleFactory<R> {
+     /**
+      * The factory to use for creating expressions.
+      */
 -    final FilterFactory<R,?,?> filterFactory;
++    final DefaultFilterFactory<R,?,?> filterFactory;
+ 
+     /**
+      * Literal commonly used as a default value.
+      *
+      * @see StyleElement#defaultToTrue(Expression)
+      * @see StyleElement#defaultToFalse(Expression)
+      */
 -    final Literal<R,Boolean> enabled, disabled;
++    final Expression<R,Boolean> enabled, disabled;
+ 
+     /**
+      * Literal commonly used as a default value.
+      */
 -    final Literal<R,Integer> zeroAsInt;
++    final Expression<R,Integer> zeroAsInt;
+ 
+     /**
+      * Literal commonly used as a default value.
+      *
+      * @see StyleElement#defaultToZero(Expression)
+      * @see StyleElement#defaultToHalf(Expression)
+      * @see StyleElement#defaultToOne(Expression)
+      */
 -    final Literal<R,Double> zero, half, one, six, ten;
++    final Expression<R,Double> zero, half, one, six, ten;
+ 
+     /**
+      * Default factor for shaded relief.
+      * This is an arbitrary suggested but not standardized by OGC 05-077r4.
+      */
 -    final Literal<R,Double> relief;
++    final Expression<R,Double> relief;
+ 
+     /**
+      * Literal commonly used as a default value.
+      */
 -    final Literal<R,String> normal, square, bevel;
++    final Expression<R,String> normal, square, bevel;
+ 
+     /**
+      * Literal for a predefined color which can be used as fill color.
+      */
 -    final Literal<R,Color> black, gray, white;
++    final Expression<R,Color> black, gray, white;
+ 
+     /**
+      * An expression for fetching the default geometry.
+      *
+      * @todo According SE specification, the default expression in the context of some symbolizers should
+      *       fetch all geometries, not only a default one. The default seems to depend on the symbolizer type.
+      */
 -    final ValueReference<R,?> defaultGeometry;
++    final Expression<R,?> defaultGeometry;
+ 
+     /**
+      * Creates a new style factory.
+      *
+      * @param  filterFactory  the factory to use for creating expressions.
+      */
 -    public StyleFactory(final FilterFactory<R,?,?> filterFactory) {
++    public StyleFactory(final DefaultFilterFactory<R,?,?> filterFactory) {
+         ArgumentChecks.ensureNonNull("filterFactory", filterFactory);
+         this.filterFactory = filterFactory;
+         enabled   = filterFactory.literal(Boolean.TRUE);
+         disabled  = filterFactory.literal(Boolean.FALSE);
+         zeroAsInt = filterFactory.literal(0);
+         zero      = filterFactory.literal(0.0);
+         half      = filterFactory.literal(0.5);
+         one       = filterFactory.literal(1.0);
+         six       = filterFactory.literal(6.0);
+         ten       = filterFactory.literal(10.0);
+         relief    = filterFactory.literal(55.0);
+         normal    = filterFactory.literal("normal");
+         square    = filterFactory.literal("square");
+         bevel     = filterFactory.literal("bevel");
+         black     = filterFactory.literal(Color.BLACK);
+         gray      = filterFactory.literal(Color.GRAY);
+         white     = filterFactory.literal(Color.WHITE);
+ 
+         defaultGeometry = filterFactory.property(AttributeConvention.GEOMETRY);
+     }
+ 
+     /**
+      * Creates a new style factory with the same literals than the given factory.
+      * This constructor shall not be public because it assumes that all literals
+      * are implementations that ignore the type {@code <R>} of data to style,
+      * in which case the unchecked cast is safe.
+      *
+      * @param  source  the style factory to copy.
+      */
+     @SuppressWarnings("unchecked")
+     StyleFactory(final StyleFactory<?> source) {
 -        enabled   = (Literal<R,Boolean>) source.enabled;
 -        disabled  = (Literal<R,Boolean>) source.disabled;
 -        zeroAsInt = (Literal<R,Integer>) source.zeroAsInt;
 -        zero      = (Literal<R,Double>)  source.zero;
 -        half      = (Literal<R,Double>)  source.half;
 -        one       = (Literal<R,Double>)  source.one;
 -        six       = (Literal<R,Double>)  source.six;
 -        ten       = (Literal<R,Double>)  source.ten;
 -        relief    = (Literal<R,Double>)  source.relief;
 -        normal    = (Literal<R,String>)  source.normal;
 -        square    = (Literal<R,String>)  source.square;
 -        bevel     = (Literal<R,String>)  source.bevel;
 -        black     = (Literal<R,Color>)   source.black;
 -        gray      = (Literal<R,Color>)   source.gray;
 -        white     = (Literal<R,Color>)   source.white;
++        enabled   = (Expression<R,Boolean>) source.enabled;
++        disabled  = (Expression<R,Boolean>) source.disabled;
++        zeroAsInt = (Expression<R,Integer>) source.zeroAsInt;
++        zero      = (Expression<R,Double>)  source.zero;
++        half      = (Expression<R,Double>)  source.half;
++        one       = (Expression<R,Double>)  source.one;
++        six       = (Expression<R,Double>)  source.six;
++        ten       = (Expression<R,Double>)  source.ten;
++        relief    = (Expression<R,Double>)  source.relief;
++        normal    = (Expression<R,String>)  source.normal;
++        square    = (Expression<R,String>)  source.square;
++        bevel     = (Expression<R,String>)  source.bevel;
++        black     = (Expression<R,Color>)   source.black;
++        gray      = (Expression<R,Color>)   source.gray;
++        white     = (Expression<R,Color>)   source.white;
+ 
+         filterFactory   = null;   // TODO: FilterFactory for coverage is not yet available.
+         defaultGeometry = null;   // Idem.
+     }
+ 
+     /**
+      * Creates an initially empty rule.
+      * A rule is a set of tendering instructions grouped by feature-property conditions and map scales.
+      *
+      * @return new initially empty rule.
+      */
+     public Rule<R> createRule() {
+         return new Rule<>(this);
+     }
+ 
+     /**
+      * Creates a point symbolizer initialized to a default graphic.
+      * A point symbolizer is a set of instructions about how to draw a graphic at a point.
+      *
+      * @return new point symbolizer initialized to a default graphic.
+      */
+     public PointSymbolizer<R> createPointSymbolizer() {
+         return new PointSymbolizer<>(this);
+     }
+ 
+     /**
+      * Creates a line symbolizer with the default stroke and no perpendicular offset.
+      * A line symbolizer is a set of instructions about how to draw on a map the lines of a geometry.
+      *
+      * @return new ine symbolizer with the default stroke and no perpendicular offset.
+      */
+     public LineSymbolizer<R> createLineSymbolizer() {
+         return new LineSymbolizer<>(this);
+     }
+ 
+     /**
+      * Creates a polygon symbolizer initialized to the default fill and default stroke.
+      * A polygon symbolizer is a set of instructions about how to draw on a map the lines and the interior of polygons.
+      *
+      * @return new polygon symbolizer initialized to the default fill and default stroke.
+      */
+     public PolygonSymbolizer<R> createPolygonSymbolizer() {
+         return new PolygonSymbolizer<>(this);
+     }
+ 
+     /**
+      * Creates a text symbolizer with default placement and default font.
+      * A text symbolizer is a set of instructions about how to drawn text on a map.
+      * The new symbolizer has no initial label.
+      *
+      * @return new text symbolizer with default placement and default font.
+      *
+      * @see #createTextSymbolizer(String)
+      */
+     public TextSymbolizer<R> createTextSymbolizer() {
+         return new TextSymbolizer<>(this);
+     }
+ 
+     /**
+      * Creates a text symbolizer initialized with the specified label literal.
+      *
+      * @param  label  initial label literal, or {@code null} if none.
+      * @return new text symbolizer with default placement and default font.
+      */
+     public TextSymbolizer<R> createTextSymbolizer(final String label) {
+         final var s = createTextSymbolizer();
+         s.label = s.literal(label);
+         return s;
+     }
+ 
+     /**
+      * Creates an initially opaque raster symbolizer with no contrast enhancement, shaded relief or outline.
+      * A raster symbolizer is a set of instructions about how to render raster, matrix or coverage data.
+      *
+      * @return new initially opaque raster symbolizer.
+      */
+     public RasterSymbolizer<R> createRasterSymbolizer() {
+         return new RasterSymbolizer<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty description.
+      * A description is a set of human-readable information about a style object being defined.
+      *
+      * @return new initially empty description.
+      */
+     public Description<R> createDescription() {
+         return new Description<>(this);
+     }
+ 
+     /**
+      * Creates a point placement initialized to anchor at the middle and no displacement.
+      * A point placement is a set of instructions about how a text label is positioned relative to a point.
+      *
+      * @return new point placement initialized to anchor at the middle and no displacement.
+      */
+     public PointPlacement<R> createPointPlacement() {
+         return new PointPlacement<>(this);
+     }
+ 
+     /**
+      * Creates a line placement initialized to no offset, no repetition and no gap.
+      * A line placement is a set of instructions about where and how a text label should be rendered relative to a line.
+      *
+      * @return new line placement initialized to no offset, no repetition and no gap.
+      */
+     public LinePlacement<R> createLinePlacement() {
+         return new LinePlacement<>(this);
+     }
+ 
+     /**
+      * Creates an anchor point initialized to <var>x</var> = 0.5 and <var>y</var> = 0.5.
+      * An anchor point is the location inside a graphic or label to use as an "anchor"
+      * for positioning it relative to a point.
+      *
+      * @return new anchor point initialized to center.
+      *
+      * @see #createAnchorPoint(double, double)
+      */
+     public AnchorPoint<R> createAnchorPoint() {
+         return new AnchorPoint<>(this);
+     }
+ 
+     /**
+      * Creates an anchor point initialized to the given position.
+      * This is a convenience method for a frequently used operation.
+      *
+      * @param  x  the initial <var>x</var> position.
+      * @param  y  the initial <var>y</var> position.
+      * @return new anchor point initialized to the given position.
+      */
+     public AnchorPoint<R> createAnchorPoint(final double x, final double y) {
+         final var s = createAnchorPoint();
+         s.anchorPointX = filterFactory.literal(x);
+         s.anchorPointY = filterFactory.literal(y);
+         return s;
+     }
+ 
+     /**
+      * Creates a displacement initialized to zero offsets.
+      * A displacement is the two-dimensional offsets from the original geometry.
+      *
+      * @return new displacement initialized to zero offsets.
+      *
+      * @see #createDisplacement(double, double)
+      */
+     public Displacement<R> createDisplacement() {
+         return new Displacement<>(this);
+     }
+ 
+     /**
+      * Creates a displacement initialized to the given offsets.
+      * This is a convenience method for a frequently used operation.
+      *
+      * @param  x  the <var>x</var> displacement.
+      * @param  y  the <var>y</var> displacement.
+      * @return new displacement initialized to the given offsets.
+      */
+     public Displacement<R> createDisplacement(final double x, final double y) {
+         final var s = createDisplacement();
+         s.displacementX = filterFactory.literal(x);
+         s.displacementY = filterFactory.literal(y);
+         return s;
+     }
+ 
+     /**
+      * Creates a mark initialized to a gray square with black outline.
+      * A mark is a predefined shape that can be drawn at the points of the geometry.
+      *
+      * @return new mark initialized to a gray square with black outline.
+      */
+     public Mark<R> createMark() {
+         return new Mark<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty external graphic.
+      * An external graphic is a reference to an external file that contains an image of some kind,
+      * such as a PNG or SVG.
+      *
+      * @return new initially empty external graphic.
+      *
+      * @see #createExternalGraphic(URI, String)
+      */
+     public ExternalGraphic<R> createExternalGraphic() {
+         return new ExternalGraphic<>(this);
+     }
+ 
+     /**
+      * Creates an external graphic initialized to the given URI.
+      *
+      * @param  linkage  URI to the external graphic, or {@code null} if none.
+      * @param  format   MIME type of the external graphic, or {@code null} if unspecified.
+      * @return new external graphic initialized to the given URI.
+      */
+     public ExternalGraphic<R> createExternalGraphic(final URI linkage, final String format) {
+         final var s = createExternalGraphic();
+         s.format = format;
+         if (linkage != null) {
+             s.onlineResource = new DefaultOnlineResource(linkage);
+         }
+         return s;
+     }
+ 
+     /**
+      * Creates a stroke initialized to solid line of black opaque color, 1 pixel width.
+      * A stroke is a set of instructions about how to draw styled lines.
+      *
+      * @return new stroke initialized to solid line of black opaque color, 1 pixel width.
+      *
+      * @see #createStroke(Color)
+      */
+     public Stroke<R> createStroke() {
+         return new Stroke<>(this);
+     }
+ 
+     /**
+      * Creates a stroke initialized to the given color and opacity.
+      * The alpha channel of the given color is used for determining the opacity.
+      *
+      * @param  color  the initial color, or {@code null} if none.
+      * @return new stroke initialized to the given color and opacity.
+      */
+     public Stroke<R> createStroke(final Color color) {
+         final var s = createStroke();
+         s.setColorAndOpacity(color);
+         return s;
+     }
+ 
+     /**
+      * Creates an opaque fill initialized to the gray color.
+      * A fill is a set of instructions about how to fill the interior of polygons.
+      *
+      * @return new opaque fill initialized to the gray color.
+      *
+      * @see #createFill(Color)
+      */
+     public Fill<R> createFill() {
+         return new Fill<>(this);
+     }
+ 
+     /**
+      * Creates a fill initialized to the given color and opacity.
+      * The alpha channel of the given color is used for determining the opacity.
+      *
+      * @param  color  the initial color, or {@code null} if none.
+      * @return new fill initialized to the given color and opacity.
+      */
+     public Fill<R> createFill(final Color color) {
+         final var s = createFill();
+         s.setColorAndOpacity(color);
+         return s;
+     }
+ 
+     /**
+      * Creates an halo initialized to a white color and a radius of 1 pixel.
+      * A halo is a fill that are applied to the backgrounds of font glyphs.
+      *
+      * @return new halo initialized to a white color and a radius of 1 pixel.
+      */
+     public Halo<R> createHalo() {
+         return new Halo<>(this);
+     }
+ 
+     /**
+      * Creates a font initialized to normal style, normal weight and a size of 10 pixels.
+      * A font is the identification of a font of a certain family, style, and size.
+      *
+      * @return new font initialized to normal style, normal weight and a size of 10 pixels.
+      */
+     public Font<R> createFont() {
+         return new Font<>(this);
+     }
+ 
+     /**
+      * Creates a graphic initialized to opaque default mark, default size and no rotation.
+      * A graphic is a symbol with an inherent shape, color(s), and possibly size.
+      *
+      * @return new graphic initialized to opaque default mark, default size and no rotation.
+      */
+     public Graphic<R> createGraphic() {
+         return new Graphic<>(this);
+     }
+ 
+     /**
+      * Creates a graphic fill initialized to a default graphic.
+      * A graphic fill is a stipple-fill repeated graphic.
+      *
+      * @return new graphic fill initialized to a default graphic.
+      */
+     public GraphicFill<R> createGraphicFill() {
+         return new GraphicFill<>(this);
+     }
+ 
+     /**
+      * Creates a graphic stroke initialized to a default graphic and no gap.
+      * A graphic stroke is a repeated-linear-graphic stroke.
+      *
+      * @return new graphic stroke initialized to a default graphic and no gap.
+      */
+     public GraphicStroke<R> createGraphicStroke() {
+         return new GraphicStroke<>(this);
+     }
+ 
+     /**
+      * Creates a legend initialized to the default graphic.
+      * A legend graphic is a graphic to do displayed in a legend for a rule.
+      *
+      * @return new legend initialized to the default graphic.
+      */
+     public LegendGraphic<R> createLegendGraphic() {
+         return new LegendGraphic<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty color replacement.
+      * A color replacement defines the replacement of a color in an external graphic.
+      *
+      * @return new initially empty color replacement.
+      */
+     public ColorReplacement<R> createColorReplacement() {
+         return new ColorReplacement<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty color map.
+      * A color map is the mapping of fixed-numeric pixel values to colors.
+      *
+      * @return new initially empty color map.
+      */
+     public ColorMap<R> createColorMap() {
+         return new ColorMap<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty channel selection.
+      * A channel selection specifies the false-color channel selection for a multi-spectral raster source.
+      *
+      * @return new initially empty channel selection.
+      */
+     public ChannelSelection<R> createChannelSelection() {
+         return new ChannelSelection<>(this);
+     }
+ 
+     /**
+      * Creates an initially empty selected channel.
+      * A selected channel is information about a channel to use in a multi-spectral source.
+      *
+      * @return new initially empty selected channel.
+      *
+      * @see #createSelectedChannel(String)
+      */
+     public SelectedChannel<R> createSelectedChannel() {
+         return new SelectedChannel<>(this);
+     }
+ 
+     /**
+      * Creates a selected channel initialized to the given channel name.
+      *
+      * @param  sourceChannelName  the channel's name, or {@code null} if unspecified.
+      * @return new selected channel for the given name.
+      */
+     public SelectedChannel<R> createSelectedChannel(final String sourceChannelName) {
+         final var s = createSelectedChannel();
+         s.sourceChannelName = s.literal(sourceChannelName);
+         return s;
+     }
+ 
+     /**
+      * Creates a contrast enhancement initialized to no operation.
+      *
+      * @return new contrast enhancement initialized to no operation.
+      */
+     public ContrastEnhancement<R> createContrastEnhancement() {
+         return new ContrastEnhancement<> (this);
+     }
+ 
+     /**
+      * Creates a shaded relief initialized to implementation-specific default values.
+      * A shaded relief is a “hill shading” applied to an image for a three-dimensional visual effect.
+      *
+      * @return new shaded relief initialized to implementation-specific default values.
+      */
+     public ShadedRelief<R> createShadedRelief() {
+         return new ShadedRelief<>(this);
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
index 0000000000,743e2d87a1..7c98bcd330
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Symbolizer.java
@@@ -1,0 -1,314 +1,314 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Objects;
+ import java.util.Optional;
+ import javax.measure.Unit;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ import jakarta.xml.bind.annotation.XmlSchemaType;
+ import jakarta.xml.bind.annotation.XmlAttribute;
+ import jakarta.xml.bind.annotation.XmlSeeAlso;
+ import org.apache.sis.measure.Units;
+ import org.apache.sis.util.resources.Errors;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++
++// Branch-depend imports
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Description of how a feature is to appear on a map.
+  * A symbolizer describes how the shape should appear,
+  * together with graphical properties such as color and opacity.
+  * A symbolizer is obtained by specifying one of a small number of different types
+  * and then supplying parameters to override its default behavior.
+  * The predefined type of symbolizers are
+  * {@linkplain LineSymbolizer line},
+  * {@linkplain PolygonSymbolizer polygon},
+  * {@linkplain PointSymbolizer point},
+  * {@linkplain TextSymbolizer text}, and
+  * {@linkplain RasterSymbolizer raster} symbolizers.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "SymbolizerType", propOrder = {
+     "name",
+     "description",
+     "geometry"
+ })
+ @XmlSeeAlso({
+     LineSymbolizer.class,
+     PointSymbolizer.class,
+     PolygonSymbolizer.class,
+     TextSymbolizer.class,
+     RasterSymbolizer.class
+ })
+ @XmlRootElement(name = "Symbolizer")
+ public abstract class Symbolizer<R> extends StyleElement<R> {
+     /**
+      * Name for this style, or {@code null} if none.
+      *
+      * @see #getName()
+      * @see #setName(String)
+      */
+     @XmlElement(name = "Name")
+     protected String name;
+ 
+     /**
+      * Information for user interfaces, or {@code null} if none.
+      *
+      * @see #getDescription()
+      * @see #setDescription(Description)
+      */
+     @XmlElement(name = "Description")
+     protected Description<R> description;
+ 
+     /**
+      * Expression fetching the geometry to draw, or {@code null} for the default geometries.
+      * The Symbology Encoding restrict the XML representation to {@code <ogc:PropertyName>}
+      * (defined in the Filter Specification), but Apache SIS accepts any expression.
+      *
+      * @see #getGeometry()
+      * @see #setGeometry(Expression)
+      */
+     @XmlElement(name = "Geometry")
+     protected Expression<R,?> geometry;
+ 
+     /**
+      * Unit of measurement for all lengths inside this symbolizer, or {@code null} for the default value.
+      * The recommended XML representations of this attribute are like below:
+      *
+      * <ul>
+      *   <li>uom="http://www.opengeospatial.org/se/units/metre"</li>
+      *   <li>uom="http://www.opengeospatial.org/se/units/foot"</li>
+      *   <li>uom="http://www.opengeospatial.org/se/units/pixel"</li>
+      * </ul>
+      *
+      * @todo Recommended XML representation is not yet implemented.
+      *
+      * @see #getUnitOfMeasure()
+      * @see #setUnitOfMeasure(Unit)
+      */
+     @XmlAttribute(name = "uom")
+     @XmlSchemaType(name = "anyURI")
+     protected Unit<?> unit;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     Symbolizer() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a symbolizer initialized to default geometries and pixel unit of measurement.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public Symbolizer(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     protected Symbolizer(final Symbolizer<R> source) {
+         super(source);
+         name        = source.name;
+         geometry    = source.geometry;
+         description = source.description;
+         unit        = source.unit;
+     }
+ 
+     /**
+      * Returns the name for this symbolizer.
+      * This can be any string that uniquely identifies this symbolizer within a given canvas.
+      * It is not meant to be human-friendly. For a human-friendly label,
+      * see the {@linkplain Description#getTitle() title} instead.
+      *
+      * @return a name for this symbolizer.
+      */
+     public Optional<String> getName() {
+         return Optional.ofNullable(name);
+     }
+ 
+     /**
+      * Sets a name for this symbolizer.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new name for this symbolizer, or {@code null} if none.
+      */
+     public void setName(final String value) {
+         name = value;
+     }
+ 
+     /**
+      * Returns the description of this style.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.
+      *
+      * @return information for user interfaces.
+      */
+     public Optional<Description<R>> getDescription() {
+         return Optional.ofNullable(description);
+     }
+ 
+     /**
+      * Sets a description of this style.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new information for user interfaces, or {@code null} if none.
+      */
+     public void setDescription(final Description<R> value) {
+         description = value;
+     }
+ 
+     /**
+      * Returns the unit of measurement for all lengths inside this symbolizer.
+      * The unit applies to stroke width, size, gap, initial gap, displacement and perpendicular offset.
+      * Two types of units are allowed:
+      *
+      * <ul>
+      *   <li>{@link Units#PIXEL} is interpreted as a paper unit, referring to the size of the map.</li>
+      *   <li>{@linkplain Units#isLinear(Unit) Linear units} such as metre and foot are “ground” units
+      *       referring to the actual size of real-world objects.</li>
+      * </ul>
+      *
+      * @return unit of measurement for all lengths inside this symbolizer.
+      */
+     public Unit<?> getUnitOfMeasure() {
+         final var value = unit;
+         return (value != null) ? value : Units.PIXEL;
+     }
+ 
+     /**
+      * Sets the unit of measurement for all lengths inside this symbolizer.
+      * If this method is never invoked, then the default value is {@link Units#PIXEL}.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new unit of measurement for lengths, or {@code null} for resetting the default value.
+      * @throws IllegalArgumentException if the specified value is neither {@link Units#PIXEL} or a linear unit.
+      */
+     public void setUnitOfMeasure(Unit<?> value) {
+         if (value != null && value != Units.PIXEL && !Units.isLinear(value)) {
+             throw new IllegalArgumentException(Errors.format(Errors.Keys.NonLinearUnit_1, value));
+         }
+         unit = value;
+     }
+ 
+     /**
+      * Returns the expression used for fetching the geometry to draw.
 -     * This expression is often a {@link ValueReference}.
+      * The value in the referenced feature property should be a geometry
+      * from a {@linkplain org.apache.sis.setup.GeometryLibrary supported library},
+      * expect in the particular case of {@link RasterSymbolizer} where the value
+      * should be a {@link org.apache.sis.coverage.BandedCoverage}.
+      *
+      * @return expression fetching the geometry or the coverage to draw.
+      */
+     public Expression<R,?> getGeometry() {
+         final var value = geometry;
+         return (value != null) ? value : factory.defaultGeometry;
+     }
+ 
+     /**
+      * Sets the expression used for fetching the geometry to draw.
+      * If this method is never invoked, then the default value is
 -     * a {@link ValueReference} fetching {@code "sis:geometry"}.
++     * a {@code ValueReference} fetching {@code "sis:geometry"}.
+      *
+      * @todo The default expression may change in a future version.
+      *       According SE specification, the default should fetch all geometries.
+      *
+      * @param  value  new expression fetching the geometry to draw, or {@code null} for resetting the default value.
+      */
+     public void setGeometry(final Expression<R,?> value) {
+         geometry = value;
+     }
+ 
+     /**
+      * Returns {@code true} if this symbolizer has enough information for displaying something.
+      *
+      * @return whether this symbolizer can display something.
+      */
+     public boolean isVisible() {
+         return true;
+     }
+ 
+     /**
+      * Returns a hash code value for this symbolizer.
+      *
+      * @return a hash code value for this symbolizer.
+      */
+     @Override
+     public int hashCode() {
+         return super.hashCode() + Objects.hash(name, description, geometry, unit);
+     }
+ 
+     /**
+      * Compares this symbolizer with the given object for equality.
+      *
+      * @param  obj  the other object to compare with this.
+      * @return whether the other object is equal to this.
+      */
+     @Override
+     public boolean equals(final Object obj) {
+         if (this == obj) {
+             return true;
+         }
+         if (!super.equals(obj)) {
+             return false;
+         }
+         final var other = (Symbolizer) obj;
+         return Objects.equals(name,        other.name)
+             && Objects.equals(description, other.description)
+             && Objects.equals(geometry,    other.geometry)
+             && Objects.equals(unit,        other.unit);
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public Symbolizer<R> clone() {
+         final var clone = (Symbolizer<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (description != null) description = description.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
index 0000000000,6a47d5c873..ffc46e799a
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/TextSymbolizer.java
@@@ -1,0 -1,288 +1,288 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.Optional;
+ import jakarta.xml.bind.annotation.XmlType;
+ import jakarta.xml.bind.annotation.XmlElement;
+ import jakarta.xml.bind.annotation.XmlElementRef;
+ import jakarta.xml.bind.annotation.XmlRootElement;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Instructions about how to drawn text on a map.
+  * The {@linkplain #getGeometry() geometry} is interpreted as being either a point
+  * or a line as needed by the {@linkplain #getLabelPlacement() label placement}.
+  * If a given geometry is not of point or line, it shall be transformed into the appropriate type.
+  *
+  * <!-- Following list of authors contains credits to OGC GeoAPI 2 contributors. -->
+  * @author  Johann Sorel (Geomatys)
+  * @author  Chris Dillard (SYS Technologies)
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ @XmlType(name = "TextSymbolizerType", propOrder = {
+     "label",
+     "font",
+     "labelPlacement",
+     "halo",
+     "fill"
+ })
+ @XmlRootElement(name = "TextSymbolizer")
+ public class TextSymbolizer<R> extends Symbolizer<R> {
+     /**
+      * Text to display, or {@code null} if none.
+      *
+      * @see #getLabel()
+      * @see #setLabel(Expression)
+      */
+     @XmlElement(name = "Label")
+     protected Expression<R,String> label;
+ 
+     /**
+      * Font to apply on the text, or {@code null} for lazily constructed default.
+      *
+      * @see #getFont()
+      * @see #setFont(Font)
+      */
+     @XmlElement(name = "Font")
+     protected Font<R> font;
+ 
+     /**
+      * Indications about how the text should be placed with respect to the feature geometry.
+      * If {@code null}, it will be lazily created when first needed.
+      *
+      * @see #getLabelPlacement()
+      * @see #setLabelPlacement(LabelPlacement)
+      */
+     @XmlElementRef(name = "LabelPlacement")
+     protected LabelPlacement<R> labelPlacement;
+ 
+     /**
+      * Indication about a halo to draw around the text, or {@code null} if none.
+      *
+      * @see #getHalo()
+      * @see #setHalo(Halo)
+      */
+     @XmlElement(name = "Halo")
+     protected Halo<R> halo;
+ 
+     /**
+      * Graphic, color and opacity of the text to draw, or {@code null} for lazily constructed default.
+      *
+      * @see #getFill()
+      * @see #setFill(Fill)
+      */
+     @XmlElement(name = "Fill")
+     protected Fill<R> fill;
+ 
+     /**
+      * For JAXB unmarshalling only.
+      */
+     private TextSymbolizer() {
+         // Thread-local factory will be used.
+     }
+ 
+     /**
+      * Creates a text symbolizer with default placement and default font.
+      * The new symbolizer has no initial label.
+      *
+      * @param  factory  the factory to use for creating expressions and child elements.
+      */
+     public TextSymbolizer(final StyleFactory<R> factory) {
+         super(factory);
+     }
+ 
+     /**
+      * Creates a shallow copy of the given object.
+      * For a deep copy, see {@link #clone()} instead.
+      *
+      * @param  source  the object to copy.
+      */
+     public TextSymbolizer(final TextSymbolizer<R> source) {
+         super(source);
+         label          = source.label;
+         font           = source.font;
+         labelPlacement = source.labelPlacement;
+         halo           = source.halo;
+         fill           = source.fill;
+     }
+ 
+     /**
+      * Returns the expression that will be evaluated to determine what text is displayed.
+      * If {@code null}, then no text will be rendered.
+      *
+      * @return text to display, or {@code null} if none.
+      *
+      * @todo Replace {@code null} by a default expression searching for a default text property in the feature.
+      */
+     public Expression<R,String> getLabel() {
+         return label;
+     }
+ 
+     /**
+      * Sets the expression that will be evaluated to determine what text is displayed.
+      * If this method is never invoked, then the default value is {@code null}.
+      *
+      * @param  value  new text to display, or {@code null} if none.
+      */
+     public void setLabel(final Expression<R,String> value) {
+         label = value;
+     }
+ 
+     /**
+      * Returns the font to apply on the text.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.
+      *
+      * @return font to apply on the text.
+      */
+     public Font<R> getFont() {
+         if (font == null) {
+             font = factory.createFont();
+         }
+         return font;
+     }
+ 
+     /**
+      * Sets the font to apply on the text.
+      * The given instance is stored by reference, it is not cloned. If this method is never invoked,
+      * then the default value is a {@linkplain Font#Font() default font}.
+      *
+      * @param  value  new font to apply on the text, or {@code null} for resetting the default value.
+      */
+     public void setFont(final Font<R> value) {
+         font = value;
+     }
+ 
+     /**
+      * Returns indications about how the text should be placed with respect to the feature geometry.
+      * This object will either be an instance of {@link LinePlacement} or {@link PointPlacement}.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.</p>
+      *
+      * @return how the text should be placed with respect to the feature geometry.
+      */
+     public LabelPlacement<R> getLabelPlacement() {
+         if (labelPlacement == null) {
+             labelPlacement = factory.createPointPlacement();
+         }
+         return labelPlacement;
+     }
+ 
+     /**
+      * Sets indications about how the text should be placed with respect to the feature geometry.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is a
+      * {@linkplain PointPlacement#PointPlacement() default point placement}.
+      *
+      * @param  value  new indications about text placement, or {@code null} for resetting the default value.
+      */
+     public void setLabelPlacement(final LabelPlacement<R> value) {
+         labelPlacement = value;
+     }
+ 
+     /**
+      * Returns indication about a halo to draw around the text.
+      *
+      * <p>The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.</p>
+      *
+      * @return indication about a halo to draw around the text.
+      */
+     public Optional<Halo<R>> getHalo() {
+         return Optional.ofNullable(halo);
+     }
+ 
+     /**
+      * Sets indication about a halo to draw around the text.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is absence.
+      *
+      * @param  value  new indication about a halo to draw around the text, or {@code null} if none.
+      */
+     public void setHalo(final Halo<R> value) {
+         halo = value;
+     }
+ 
+     /**
+      * Returns the graphic, color and opacity of the text to draw.
+      * The returned object is <em>live</em>:
+      * changes in the returned instance will be reflected in this symbolizer, and conversely.
+      *
+      * @return graphic, color and opacity of the text to draw.
+      */
+     public Fill<R> getFill() {
+         if (fill == null) {
+             fill = factory.createFill();
+             fill.setColor(factory.black);
+         }
+         return fill;
+     }
+ 
+     /**
+      * Sets the graphic, color and opacity of the text to draw.
+      * The given instance is stored by reference, it is not cloned.
+      * If this method is never invoked, then the default value is a solid black color.
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new fill of the text to draw, or {@code null} for resetting the default value.
+      */
+     public void setFill(final Fill<R> value) {
+         fill = value;
+     }
+ 
+     /**
+      * Returns all properties contained in this class.
+      * This is used for {@link #equals(Object)} and {@link #hashCode()} implementations.
+      */
+     @Override
+     final Object[] properties() {
+         return new Object[] {label, font, labelPlacement, halo, fill};
+     }
+ 
+     /**
+      * Returns a deep clone of this object. All style elements are cloned,
+      * but expressions are not on the assumption that they are immutable.
+      *
+      * @return deep clone of all style elements.
+      */
+     @Override
+     public TextSymbolizer<R> clone() {
+         final var clone = (TextSymbolizer<R>) super.clone();
+         clone.selfClone();
+         return clone;
+     }
+ 
+     /**
+      * Clones the mutable style fields of this element.
+      */
+     private void selfClone() {
+         if (font           != null) font           = font.clone();
+         if (labelPlacement != null) labelPlacement = labelPlacement.clone();
+         if (halo           != null) halo           = halo.clone();
+         if (fill           != null) fill           = fill.clone();
+     }
+ }
diff --cc core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
index 0000000000,f8d9e11624..5d90081d36
mode 000000,100644..100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/style/se1/Translucent.java
@@@ -1,0 -1,49 +1,49 @@@
+ /*
+  * 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.style.se1;
+ 
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
+ 
+ 
+ /**
+  * Object which can be rendered with various levels of transparency.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @param <R>  the type of data to style, such as {@code Feature} or {@code Coverage}.
+  *
+  * @since 1.5
+  */
+ public interface Translucent<R> {
+     /**
+      * Indicates the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * A value of zero means completely transparent. A value of 1.0 means completely opaque.
+      *
+      * @return the level of translucency as a floating point number between 0 and 1 (inclusive).
+      */
+     Expression<R, ? extends Number> getOpacity();
+ 
+     /**
+      * Sets the level of translucency as a floating point number between 0 and 1 (inclusive).
+      * If this method is never invoked, then the default value is literal 1 (totally opaque).
+      * That default value is standardized by OGC 05-077r4.
+      *
+      * @param  value  new level of translucency, or {@code null} for resetting the default value.
+      */
+     void setOpacity(Expression<R, ? extends Number> value);
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
index 0000000000,a0e2fe0f62..3e925ff721
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/HaloTest.java
@@@ -1,0 -1,73 +1,73 @@@
+ /*
+  * 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.style.se1;
+ 
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link Halo}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class HaloTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public HaloTest() {
+     }
+ 
+     /**
+      * Test of {@code Fill} property.
+      */
+     @Test
+     public void testFill() {
+         final var cdt = factory.createHalo();
+ 
+         // Check default
 -        Fill<Feature> value = cdt.getFill();
++        Fill<AbstractFeature> value = cdt.getFill();
+         assertEquals(factory.white, value.getColor());
+         assertLiteralEquals(1.0, value.getOpacity());
+ 
+         // Check get/set
+         value.setColor(anyColor());
+         value.setOpacity(literal(0.8));
+         cdt.setFill(value);
+         assertEquals(value, cdt.getFill());
+     }
+ 
+     /**
+      * Test of {@code Radius} property.
+      */
+     @Test
+     public void testRadius() {
+         final var cdt = factory.createHalo();
+ 
+         // Check default
+         assertLiteralEquals(1.0, cdt.getRadius());
+ 
+         // Check get/set
+         cdt.setRadius(literal(40));
+         assertLiteralEquals(40, cdt.getRadius());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
index 0000000000,9a71c7282e..11ddf31d3a
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/LineSymbolizerTest.java
@@@ -1,0 -1,72 +1,72 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link LineSymbolizer}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class LineSymbolizerTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public LineSymbolizerTest() {
+     }
+ 
+     /**
+      * Test of {@code Stroke} property.
+      */
+     @Test
+     public void testStroke() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check default
 -        Stroke<Feature> value = cdt.getStroke();
++        Stroke<AbstractFeature> value = cdt.getStroke();
+         assertLiteralEquals(Color.BLACK, value.getColor());
+ 
+         // Check get/set
+         value = factory.createStroke();
+         cdt.setStroke(value);
+         assertEquals(value, cdt.getStroke());
+     }
+ 
+     /**
+      * Test of {@code PerpendicularOffset} property.
+      */
+     @Test
+     public void testPerpendicularOffset() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check default
+         assertLiteralEquals(0.0, cdt.getPerpendicularOffset());
+ 
+         // Check get/set
+         cdt.setPerpendicularOffset(literal(20));
+         assertLiteralEquals(20, cdt.getPerpendicularOffset());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
index 0000000000,3b4a8729ab..7ce1a72408
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointPlacementTest.java
@@@ -1,0 -1,90 +1,90 @@@
+ /*
+  * 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.style.se1;
+ 
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link PointPlacement}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class PointPlacementTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public PointPlacementTest() {
+     }
+ 
+     /**
+      * Test of {@code AnchorPoint} property.
+      */
+     @Test
+     public void testAnchorPoint() {
+         final var cdt = factory.createPointPlacement();
+ 
+         // Check default
 -        AnchorPoint<Feature> value = cdt.getAnchorPoint();
++        AnchorPoint<AbstractFeature> value = cdt.getAnchorPoint();
+         assertLiteralEquals(0.5, value.getAnchorPointX());
+         assertLiteralEquals(0.5, value.getAnchorPointY());
+ 
+         // Check get/set
+         value = factory.createAnchorPoint(3, 1);
+         cdt.setAnchorPoint(value);
+         assertEquals(value, cdt.getAnchorPoint());
+     }
+ 
+     /**
+      * Test of {@code Displacement} property.
+      */
+     @Test
+     public void testDisplacement() {
+         final var cdt = factory.createPointPlacement();
+ 
+         // Check default
 -        Displacement<Feature> value = cdt.getDisplacement();
++        Displacement<AbstractFeature> value = cdt.getDisplacement();
+         assertLiteralEquals(0.0, value.getDisplacementX());
+         assertLiteralEquals(0.0, value.getDisplacementY());
+ 
+         // Check get/set
+         value = factory.createDisplacement(1, 2);
+         cdt.setDisplacement(value);
+         assertEquals(value, cdt.getDisplacement());
+     }
+ 
+     /**
+      * Test of {@code Rotation} property.
+      */
+     @Test
+     public void testRotation() {
+         final var cdt = factory.createPointPlacement();
+ 
+         // Check default
+         assertLiteralEquals(0.0, cdt.getRotation());
+ 
+         // Check get/set
+         cdt.setRotation(literal(180));
+         assertLiteralEquals(180, cdt.getRotation());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
index 0000000000,44598828eb..d72d63ba2a
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PointSymbolizerTest.java
@@@ -1,0 -1,57 +1,57 @@@
+ /*
+  * 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.style.se1;
+ 
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link PointSymbolizer}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class PointSymbolizerTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public PointSymbolizerTest() {
+     }
+ 
+     /**
+      * Test of {@code Graphic} property.
+      */
+     @Test
+     public void testGraphic() {
+         final var cdt = factory.createPointSymbolizer();
+ 
+         // Check default
 -        Graphic<Feature> value = cdt.getGraphic();
++        Graphic<AbstractFeature> value = cdt.getGraphic();
+         assertLiteralEquals(1.0, value.getOpacity());
+ 
+         // Check get/set
+         value = factory.createGraphic();
+         value.setOpacity(literal(0.8));
+         cdt.setGraphic(value);
+         assertEquals(value, cdt.getGraphic());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
index 0000000000,08fc95a730..073f449607
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/PolygonSymbolizerTest.java
@@@ -1,0 -1,109 +1,109 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link PolygonSymbolizer}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class PolygonSymbolizerTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public PolygonSymbolizerTest() {
+     }
+ 
+     /**
+      * Test of {@code Stroke} property.
+      */
+     @Test
+     public void testStroke() {
+         final var cdt = factory.createPolygonSymbolizer();
+ 
+         // Check default
+         var value = cdt.getStroke().orElseThrow();
+         assertLiteralEquals("bevel",  value.getLineJoin());
+         assertLiteralEquals("square", value.getLineCap());
+ 
+         // Check get/set
+         value = factory.createStroke();
+         cdt.setStroke(value);
+         assertOptionalEquals(value, cdt.getStroke());
+     }
+ 
+     /**
+      * Test of {@code Fill} property.
+      */
+     @Test
+     public void testFill() {
+         final var cdt = factory.createPolygonSymbolizer();
+ 
+         // Check default
 -        Fill<Feature> value = cdt.getFill().orElseThrow();
++        Fill<AbstractFeature> value = cdt.getFill().orElseThrow();
+         assertLiteralEquals(Color.GRAY, value.getColor());
+ 
+         // Check get/set
+         value = factory.createFill();
+         value.setColorAndOpacity(ANY_COLOR);
+         cdt.setFill(value);
+         assertOptionalEquals(value, cdt.getFill());
+     }
+ 
+     /**
+      * Test of {@code Displacement} property.
+      */
+     @Test
+     public void testDisplacement() {
+         final var cdt = factory.createPolygonSymbolizer();
+ 
+         // Check default
 -        Displacement<Feature> value = cdt.getDisplacement();
++        Displacement<AbstractFeature> value = cdt.getDisplacement();
+         assertLiteralEquals(0.0, value.getDisplacementX());
+         assertLiteralEquals(0.0, value.getDisplacementY());
+ 
+         // Check get/set
+         value = factory.createDisplacement(4, 1);
+         cdt.setDisplacement(value);
+         assertEquals(value, cdt.getDisplacement());
+     }
+ 
+     /**
+      * Test of {@code PerpendicularOffset} property.
+      */
+     @Test
+     public void testPerpendicularOffset() {
+         final var cdt = factory.createPolygonSymbolizer();
+ 
+         // Check default
+         assertLiteralEquals(0.0, cdt.getPerpendicularOffset());
+ 
+         // Check get/set
+         cdt.setPerpendicularOffset(literal(10));
+         assertLiteralEquals(10, cdt.getPerpendicularOffset());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
index 0000000000,c4ccfc7da2..cfd0c69de3
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/RuleTest.java
@@@ -1,0 -1,184 +1,184 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.util.List;
+ import org.junit.Test;
+ import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
+ 
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.Filter;
++import org.apache.sis.filter.Filter;
+ 
+ 
+ /**
+  * Tests for {@link Rule}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class RuleTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public RuleTest() {
+     }
+ 
+     /**
+      * Test of {@code Name} property.
+      */
+     @Test
+     public void testGetName() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEmpty(cdt.getName());
+ 
+         // Check get/set
+         String value = "A random name";
+         cdt.setName(value);
+         assertOptionalEquals(value, cdt.getName());
+     }
+ 
+     /**
+      * Test of {@code Description} property.
+      */
+     @Test
+     public void testDescription() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEmpty(cdt.getDescription());
+ 
+         // Check get/set
+         var desc = anyDescription();
+         cdt.setDescription(desc);
+         assertOptionalEquals(desc, cdt.getDescription());
+     }
+ 
+     /**
+      * Test of {@code Legend} property.
+      */
+     @Test
+     public void testLegend() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEmpty(cdt.getLegend());
+ 
+         // Check get/set
+         var value = factory.createLegendGraphic();
+         cdt.setLegend(value);
+         assertOptionalEquals(value, cdt.getLegend());
+     }
+ 
+     /**
+      * Test of {@code Filter} property.
+      */
+     @Test
+     public void testFilter() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEquals(Filter.include(), cdt.getFilter());
+ 
+         // Check get/set
+         var value = factory.filterFactory.equal(literal("A"), literal("B"));
+         cdt.setFilter(value);
+         assertEquals(value, cdt.getFilter());
+     }
+ 
+     /**
+      * Test of {@code ElseFilter} property.
+      */
+     @Test
+     public void testIsElseFilter() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertFalse(cdt.isElseFilter());
+ 
+         // Check get/set
+         cdt.setElseFilter(true);
+         assertTrue(cdt.isElseFilter());
+     }
+ 
+     /**
+      * Test of {@code MinScaleDenominator} property.
+      */
+     @Test
+     public void testMinScaleDenominator() {
+         final var cdt = factory.createRule();
+ 
+         // 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 {@code MaxScaleDenominator} property.
+      */
+     @Test
+     public void testGetMaxScaleDenominator() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEquals(Double.POSITIVE_INFINITY, cdt.getMaxScaleDenominator(), 0.0);
+ 
+         // Check get/set
+         cdt.setMaxScaleDenominator(10.0);
+         assertEquals(10.0, cdt.getMaxScaleDenominator(), 0.0);
+     }
+ 
+     /**
+      * Test of {@code Symbolizer} property.
+      */
+     @Test
+     public void testSymbolizers() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertTrue(cdt.symbolizers().isEmpty());
+ 
+         // Check get/set
+         var value = factory.createLineSymbolizer();
+         cdt.symbolizers().add(value);
+         assertEquals(List.of(value), cdt.symbolizers());
+     }
+ 
+     /**
+      * Test of {@code OnlineSource} property.
+      */
+     @Test
+     public void testGetOnlineSource() {
+         final var cdt = factory.createRule();
+ 
+         // Check defaults
+         assertEmpty(cdt.getOnlineSource());
+ 
+         // Check get/set
+         var r = new DefaultOnlineResource();
+         r.setProtocol("HTTP");
+         cdt.setOnlineSource(r);
+         assertOptionalEquals(r, cdt.getOnlineSource());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
index 0000000000,bd2fe221ca..5737ee6722
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/StyleTestCase.java
@@@ -1,0 -1,120 +1,120 @@@
+ /*
+  * 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.style.se1;
+ 
+ import java.awt.Color;
+ import java.util.Optional;
+ import org.apache.sis.util.SimpleInternationalString;
+ import org.apache.sis.test.TestCase;
+ 
+ import static org.junit.Assert.*;
+ import static org.opengis.test.Assert.assertInstanceOf;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.internal.geoapi.filter.Literal;
+ 
+ 
+ /**
+  * Base class of all tests of style elements.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ abstract class StyleTestCase extends TestCase {
+     /**
+      * The factory to use for creating style elements.
+      */
 -    final StyleFactory<Feature> factory;
++    final StyleFactory<AbstractFeature> factory;
+ 
+     /**
+      * Creates a new test case.
+      */
+     StyleTestCase() {
+         factory = FeatureTypeStyle.FACTORY;
+     }
+ 
+     /**
+      * Returns a literal for the given value.
+      *
+      * @param  <E>    type of value.
+      * @param  value  the value for which to return a literal.
+      * @return literal for the given value.
+      */
 -    final <E> Literal<Feature,E> literal(final E value) {
++    final <E> Expression<AbstractFeature,E> literal(final E value) {
+         return factory.filterFactory.literal(value);
+     }
+ 
+     /**
+      * Creates a dummy description with arbitrary title and abstract.
+      */
 -    final Description<Feature> anyDescription() {
++    final Description<AbstractFeature> anyDescription() {
+         final var value = factory.createDescription();
+         value.setTitle(new SimpleInternationalString("A random title"));
+         value.setAbstract(new SimpleInternationalString("A random abstract"));
+         return value;
+     }
+ 
+     /**
+      * Returns an expression with a random color.
+      * The color is {@link #ANY_COLOR}.
+      */
 -    final Expression<Feature,Color> anyColor() {
++    final Expression<AbstractFeature,Color> anyColor() {
+         return literal(ANY_COLOR);
+     }
+ 
+     /**
+      * The color used by {@link #anyColor()}.
+      * Should be different than all default colors.
+      * Provided for verification with {@link #assertLiteralEquals(Object, Expression)}.
+      */
+     static final Color ANY_COLOR = Color.YELLOW;
+ 
+     /**
+      * Asserts that the given optional is empty.
+      *
+      * @param  opt  the optional to check.
+      */
+     static void assertEmpty(final Optional<?> opt) {
+         assertTrue(opt.isEmpty());
+     }
+ 
+     /**
+      * Asserts that the value of the given optional is equals to the expected value.
+      *
+      * @param <E>       type of object to compare.
+      * @param expected  the expected value.
+      * @param opt       the actual value in an optional.
+      */
+     static <E> void assertOptionalEquals(final E expected, final Optional<E> opt) {
+         assertEquals(expected, opt.orElseThrow());
+     }
+ 
+     /**
+      * Asserts that the given expression is a literal with the given value.
+      *
+      * @param  <E>       the expression value type.
+      * @param  expected  the expected expression value.
+      * @param  actual    the expression from which to test the value.
+      */
+     static <E> void assertLiteralEquals(final E expected, final Expression<?, ? extends E> actual) {
+         assertInstanceOf("expression", Literal.class, actual);
+         assertEquals(expected, actual.apply(null));
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
index 0000000000,d9889903b0..39012eb3b6
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/SymbolizerTest.java
@@@ -1,0 -1,103 +1,103 @@@
+ /*
+  * 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.style.se1;
+ 
+ import org.apache.sis.measure.Units;
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ import static org.opengis.test.Assert.assertInstanceOf;
+ 
+ // Branch-dependent imports
 -import org.opengis.filter.ValueReference;
++import org.apache.sis.internal.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * Tests for {@link Symbolizer}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class SymbolizerTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public SymbolizerTest() {
+     }
+ 
+     /**
+      * Test of {@code UnitOfMeasure} property.
+      */
+     @Test
+     public void testUnitOfMeasure() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check default
+         assertEquals(Units.PIXEL, cdt.getUnitOfMeasure());
+ 
+         // Check get/set
+         cdt.setUnitOfMeasure(Units.INCH);
+         assertEquals(Units.INCH, cdt.getUnitOfMeasure());
+     }
+ 
+     /**
+      * Test of {@code Geometry} property.
+      */
+     @Test
+     public void testGeometry() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check default
+         assertInstanceOf("geometry", ValueReference.class, cdt.getGeometry());
+ 
+         // Check get/set
+         cdt.setGeometry(literal(8));
+         assertLiteralEquals(8, cdt.getGeometry());
+     }
+ 
+     /**
+      * Test of {@code Name} property.
+      */
+     @Test
+     public void testName() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check defaults
+         assertEmpty(cdt.getName());
+ 
+         // Check get/set
+         String value = "A random name";
+         cdt.setName(value);
+         assertOptionalEquals(value, cdt.getName());
+     }
+ 
+     /**
+      * Test of {@code getDescription} property.
+      */
+     @Test
+     public void testDescription() {
+         final var cdt = factory.createLineSymbolizer();
+ 
+         // Check defaults
+         assertEmpty(cdt.getDescription());
+ 
+         // Check get/set
+         var desc = anyDescription();
+         cdt.setDescription(desc);
+         assertOptionalEquals(desc, cdt.getDescription());
+     }
+ }
diff --cc core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
index 0000000000,5954377dcd..1e23bd6342
mode 000000,100644..100644
--- a/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
+++ b/core/sis-portrayal/src/test/java/org/apache/sis/style/se1/TextSymbolizerTest.java
@@@ -1,0 -1,122 +1,122 @@@
+ /*
+  * 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.style.se1;
+ 
+ import org.junit.Test;
+ import static org.junit.Assert.*;
+ 
+ // Branch-dependent imports
 -import org.opengis.feature.Feature;
++import org.apache.sis.feature.AbstractFeature;
+ 
+ 
+ /**
+  * Tests for {@link TextSymbolizer}.
+  *
+  * @author  Johann Sorel (Geomatys)
+  * @version 1.5
+  * @since   1.5
+  */
+ public final class TextSymbolizerTest extends StyleTestCase {
+     /**
+      * Creates a new test case.
+      */
+     public TextSymbolizerTest() {
+     }
+ 
+     /**
+      * Test of {@code Label} property.
+      */
+     @Test
+     public void testLabel() {
+         final var cdt = factory.createTextSymbolizer();
+ 
+         // Check default
+         assertNull(cdt.getLabel());
+ 
+         // Check get/set
+         var value = literal("A random label");
+         cdt.setLabel(value);
+         assertEquals(value, cdt.getLabel());
+     }
+ 
+     /**
+      * Test of {@code Font} property.
+      */
+     @Test
+     public void testFont() {
+         final var cdt = factory.createTextSymbolizer();
+ 
+         // Check default
+         var value = cdt.getFont();
+         assertLiteralEquals("normal", value.getStyle());
+ 
+         // Check get/set
+         value = factory.createFont();
+         value.setStyle(literal("italic"));
+         cdt.setFont(value);
+         assertEquals(value, cdt.getFont());
+     }
+ 
+     /**
+      * Test of {@code LabelPlacement} property.
+      */
+     @Test
+     public void testLabelPlacement() {
+         final var cdt = factory.createTextSymbolizer();
+ 
+         // Check default
 -        LabelPlacement<Feature> value = cdt.getLabelPlacement();
++        LabelPlacement<AbstractFeature> value = cdt.getLabelPlacement();
+         assertNotNull(value);
+ 
+         // Check get/set
+         value = factory.createPointPlacement();
+         cdt.setLabelPlacement(value);
+         assertEquals(value, cdt.getLabelPlacement());
+     }
+ 
+     /**
+      * Test of {@code Halo} property.
+      */
+     @Test
+     public void testHalo() {
+         final var cdt = factory.createTextSymbolizer();
+ 
+         // Check default
+         assertEmpty(cdt.getHalo());
+ 
+         // Check get/set
+         var value = factory.createHalo();
+         cdt.setHalo(value);
+         assertOptionalEquals(value, cdt.getHalo());
+     }
+ 
+     /**
+      * Test of {@code Fill} property.
+      */
+     @Test
+     public void testFill() {
+         final var cdt = factory.createTextSymbolizer();
+ 
+         // Check default
+         assertNotNull(cdt.getFill());
+ 
+         // Check get/set
+         var value = factory.createFill();
+         cdt.setFill(value);
+         assertEquals(value, cdt.getFill());
+     }
+ }