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

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

commit 8043b8130f1b8394fa951fe11d8d42b552b27458
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Oct 27 15:00:19 2023 +0200

    Rearrange the GeoTIFF internal classes in some subpackages
    in order to provide at least a partial separation between
    reader and writer classes.
---
 .../org/apache/sis/storage/geotiff/DataCube.java   |  7 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |  3 +-
 .../apache/sis/storage/geotiff/DeferredEntry.java  |  2 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   | 95 ++++++++++++----------
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |  8 +-
 .../storage/geotiff/{GeoTIFF.java => IOBase.java}  | 52 +++---------
 .../sis/storage/geotiff/ImageFileDirectory.java    | 40 ++++++---
 .../apache/sis/storage/geotiff/NativeMetadata.java | 15 ++--
 .../org/apache/sis/storage/geotiff/Reader.java     | 22 ++++-
 .../org/apache/sis/storage/geotiff/Writer.java     | 79 +++++++++---------
 .../geotiff/{internal => base}/Compression.java    |  2 +-
 .../sis/storage/geotiff/{ => base}/GeoCodes.java   | 33 +++++++-
 .../sis/storage/geotiff/{ => base}/GeoKeys.java    | 19 +----
 .../geotiff/{internal => base}/Predictor.java      |  2 +-
 .../geotiff/{internal => base}/Resources.java      |  2 +-
 .../{internal => base}/Resources.properties        |  0
 .../geotiff/{internal => base}/Resources_en.java   |  2 +-
 .../geotiff/{internal => base}/Resources_fr.java   |  2 +-
 .../{internal => base}/Resources_fr.properties     |  0
 .../sis/storage/geotiff/{ => base}/Tags.java       |  8 +-
 .../sis/storage/geotiff/{ => base}/UnitKey.java    | 16 ++--
 .../geotiff/{internal => base}/package-info.java   |  4 +-
 .../geotiff/inflater/CompressionChannel.java       |  2 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |  6 +-
 .../apache/sis/storage/geotiff/inflater/LZW.java   |  2 +-
 .../storage/geotiff/inflater/PredictorChannel.java |  2 +-
 .../storage/geotiff/{ => reader}/CRSBuilder.java   | 50 ++++++++----
 .../geotiff/{ => reader}/GeoKeysLoader.java        | 34 +++-----
 .../geotiff/{ => reader}/GridGeometryBuilder.java  | 38 +++++----
 .../geotiff/{ => reader}/ImageMetadataBuilder.java | 46 ++++-------
 .../storage/geotiff/{ => reader}/Localization.java |  2 +-
 .../geotiff/{ => reader}/ReversedBitsChannel.java  |  9 +-
 .../sis/storage/geotiff/{ => reader}/Type.java     |  8 +-
 .../storage/geotiff/{ => reader}/XMLMetadata.java  | 39 +++++----
 .../geotiff/{internal => reader}/package-info.java | 10 ++-
 .../{GeoKeysWriter.java => writer/GeoEncoder.java} | 76 +++++++++--------
 .../geotiff/{ => writer}/ReformattedImage.java     | 22 ++---
 .../{TagValueWriter.java => writer/TagValue.java}  | 52 ++++++++++--
 .../TileMatrix.java}                               | 22 ++---
 .../geotiff/{internal => writer}/package-info.java | 10 ++-
 .../org/apache/sis/storage/geotiff/WriterTest.java | 11 +--
 .../{internal => base}/CompressionTest.java        |  2 +-
 .../storage/geotiff/{ => base}/GeoCodesTest.java   |  2 +-
 .../storage/geotiff/{ => base}/GeoIdentifiers.java |  2 +-
 .../storage/geotiff/{ => base}/GeoKeysTest.java    |  6 +-
 .../sis/storage/geotiff/{ => base}/TagsTest.java   |  2 +-
 .../geotiff/{ => reader}/CRSBuilderTest.java       |  2 +-
 .../sis/storage/geotiff/{ => reader}/TypeTest.java |  2 +-
 .../geotiff/{ => reader}/XMLMetadataTest.java      |  2 +-
 .../apache/sis/storage/base/MetadataFetcher.java   |  4 +-
 50 files changed, 485 insertions(+), 393 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
index eff8a80e3e..43980dd29e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataCube.java
@@ -28,9 +28,10 @@ import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
-import org.apache.sis.storage.geotiff.internal.Resources;
-import org.apache.sis.storage.geotiff.internal.Predictor;
-import org.apache.sis.storage.geotiff.internal.Compression;
+import org.apache.sis.storage.geotiff.base.Tags;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.geotiff.base.Predictor;
+import org.apache.sis.storage.geotiff.base.Compression;
 import org.apache.sis.storage.base.TiledGridResource;
 import org.apache.sis.storage.base.ResourceOnFileSystem;
 import org.apache.sis.storage.base.StoreResource;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
index def938762b..e58172a29d 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
@@ -42,7 +42,8 @@ import org.apache.sis.storage.base.TiledGridResource;
 import org.apache.sis.coverage.grid.j2d.TilePlaceholder;
 import org.apache.sis.coverage.grid.j2d.ImageUtilities;
 import org.apache.sis.coverage.grid.j2d.RasterFactory;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.geotiff.reader.ReversedBitsChannel;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Vector;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DeferredEntry.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DeferredEntry.java
index 7b4c5d9396..b6f719b76f 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DeferredEntry.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DeferredEntry.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.storage.geotiff;
 
+import org.apache.sis.storage.geotiff.reader.Type;
+
 
 /**
  * Offset to a TIFF tag entry that has not yet been read.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index e6ef201639..b787aac7b2 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -19,8 +19,10 @@ package org.apache.sis.storage.geotiff;
 import java.util.Set;
 import java.util.List;
 import java.util.Locale;
+import java.util.TimeZone;
 import java.util.Optional;
-import java.util.logging.LogRecord;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.net.URI;
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -107,6 +109,18 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      */
     final Locale dataLocale;
 
+    /**
+     * The timezone for the date and time parsing, or {@code null} for the 
default.
+     */
+    private final TimeZone timezone;
+
+    /**
+     * The object to use for parsing and formatting dates. Created when first 
needed.
+     *
+     * @see #getDateFormat()
+     */
+    private transient DateFormat dateFormat;
+
     /**
      * The {@link GeoTiffStoreProvider#LOCATION} parameter value, or {@code 
null} if none.
      * This is used for information purpose only, not for actual reading 
operations.
@@ -227,6 +241,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         this.encoding = (encoding != null) ? encoding : 
StandardCharsets.US_ASCII;
 
         dataLocale = connector.getOption(OptionKey.LOCALE);
+        timezone   = connector.getOption(OptionKey.TIMEZONE);
         location   = connector.getStorageAs(URI.class);
         path       = connector.getStorageAs(Path.class);
         try {
@@ -284,8 +299,6 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
 
     /**
      * Opens access to listeners for {@link ImageFileDirectory}.
-     *
-     * @see #warning(LogRecord)
      */
     final StoreListeners listeners() {
         return listeners;
@@ -424,21 +437,16 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
     }
 
     /**
-     * Returns the exception to throw when an I/O error occurred.
-     * This method wraps the exception with a {@literal "Cannot read 
<filename>"} message.
+     * {@return the object to use for parsing and formatting dates}.
      */
-    final DataStoreException errorIO(final IOException e) {
-        return new 
DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, 
getDisplayName()), e);
-    }
-
-    /**
-     * Returns a localized error message saying that this data store has been 
opened in read-only or write-only mode.
-     *
-     * @param  mode  0 for read-only, or 1 for write-only.
-     * @return localized error message.
-     */
-    final String readOrWriteOnly(final int mode) {
-        return errors().getString(Errors.Keys.OpenedReadOrWriteOnly_2, mode, 
getDisplayName());
+    final DateFormat getDateFormat() {
+        if (dateFormat == null) {
+            dateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", 
Locale.US);
+            if (timezone != null) {
+                dateFormat.setTimeZone(timezone);
+            }
+        }
+        return dateFormat;
     }
 
     /**
@@ -707,6 +715,31 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         }
     }
 
+    /**
+     * Returns the error resources in the current locale.
+     */
+    private Errors errors() {
+        return Errors.getResources(getLocale());
+    }
+
+    /**
+     * Returns the exception to throw when an I/O error occurred.
+     * This method wraps the exception with a {@literal "Cannot read 
<filename>"} message.
+     */
+    final DataStoreException errorIO(final IOException e) {
+        return new 
DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, 
getDisplayName()), e);
+    }
+
+    /**
+     * Returns a localized error message saying that this data store has been 
opened in read-only or write-only mode.
+     *
+     * @param  mode  0 for read-only, or 1 for write-only.
+     * @return localized error message.
+     */
+    final String readOrWriteOnly(final int mode) {
+        return errors().getString(Errors.Keys.OpenedReadOrWriteOnly_2, mode, 
getDisplayName());
+    }
+
     /**
      * Closes this GeoTIFF store and releases any underlying resources.
      * This method can be invoked asynchronously for interrupting a long 
reading process.
@@ -734,32 +767,4 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             }
         }
     }
-
-    /**
-     * Returns the error resources in the current locale.
-     */
-    final Errors errors() {
-        return Errors.getResources(getLocale());
-    }
-
-    /**
-     * Reports a warning contained in the given {@link LogRecord}.
-     * Note that the given record will not necessarily be sent to the logging 
framework;
-     * if the user has registered at least one listener, then the record will 
be sent to the listeners instead.
-     *
-     * <p>This method sets the {@linkplain 
LogRecord#setSourceClassName(String) source class name} and
-     * {@linkplain LogRecord#setSourceMethodName(String) source method name} 
to hard-coded values.
-     * Those values assume that the warnings occurred indirectly from a call 
to {@link #getMetadata()}
-     * in this class. We do not report private classes or methods as the 
source of warnings.</p>
-     *
-     * @param  record  the warning to report.
-     *
-     * @see #listeners()
-     */
-    final void warning(final LogRecord record) {
-        // Logger name will be set by listeners.warning(record).
-        record.setSourceClassName(GeoTiffStore.class.getName());
-        record.setSourceMethodName("getMetadata");
-        listeners.warning(record);
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
index 1c130998e6..b7a5c5d723 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
@@ -135,11 +135,11 @@ public class GeoTiffStoreProvider extends 
DataStoreProvider {
                 return ProbeResult.INSUFFICIENT_BYTES;
             }
             switch (buffer.getShort()) {
-                case GeoTIFF.LITTLE_ENDIAN: 
buffer.order(ByteOrder.LITTLE_ENDIAN);      // Fall through
-                case GeoTIFF.BIG_ENDIAN: {  // Default buffer order is big 
endian.
+                case IOBase.LITTLE_ENDIAN: 
buffer.order(ByteOrder.LITTLE_ENDIAN);       // Fall through
+                case IOBase.BIG_ENDIAN: {   // Default buffer order is big 
endian.
                     switch (buffer.getShort()) {
-                        case GeoTIFF.CLASSIC:
-                        case GeoTIFF.BIG_TIFF: return new ProbeResult(true, 
MIME_TYPE, VERSION);
+                        case IOBase.CLASSIC:
+                        case IOBase.BIG_TIFF: return new ProbeResult(true, 
MIME_TYPE, VERSION);
                     }
                 }
             }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTIFF.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/IOBase.java
similarity index 57%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTIFF.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/IOBase.java
index 493acb1bc5..49d611cccd 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTIFF.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/IOBase.java
@@ -17,13 +17,9 @@
 package org.apache.sis.storage.geotiff;
 
 import java.util.Set;
-import java.util.Locale;
-import java.util.TimeZone;
 import java.io.Closeable;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Resources;
 
 
 /**
@@ -36,44 +32,29 @@ import org.apache.sis.storage.geotiff.internal.Resources;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-abstract class GeoTIFF implements Closeable {
-    /**
-     * The timezone for the date and time parsing, or {@code null} for the 
default.
-     * This is not yet configurable, but may become in a future version.
-     */
-    private static final TimeZone TIMEZONE = null;
-
-    /**
-     * The locale to use for parsers or formatter. This is 
<strong>not</strong> the locale
-     * for warnings or other messages emitted to the users.
-     */
-    private static final Locale LOCALE = Locale.US;
-
+abstract class IOBase implements Closeable {
     /**
      * The magic number for big-endian TIFF files or little-endian TIFF files.
      */
-    static final short BIG_ENDIAN = 0x4D4D, LITTLE_ENDIAN = 0x4949;
+    protected static final short BIG_ENDIAN = 0x4D4D, LITTLE_ENDIAN = 0x4949;
 
     /**
      * The magic number for classic (32 bits) or big TIFF (64 bits) files.
      */
-    static final short CLASSIC = 42, BIG_TIFF= 43;
+    protected static final short CLASSIC = 42, BIG_TIFF= 43;
 
     /**
      * The store which created this reader or writer.
      * This is also the synchronization lock.
      */
-    final GeoTiffStore store;
-
-    /**
-     * The object to use for parsing and formatting dates. Created when first 
needed.
-     */
-    private transient DateFormat dateFormat;
+    public final GeoTiffStore store;
 
     /**
      * For subclass constructors.
+     *
+     * @param  store  the store which created this reader or writer.
      */
-    GeoTIFF(final GeoTiffStore store) {
+    protected IOBase(final GeoTiffStore store) {
         this.store = store;
     }
 
@@ -85,29 +66,16 @@ abstract class GeoTIFF implements Closeable {
     public abstract Set<GeoTiffOption> getOptions();
 
     /**
-     * Returns the resources to use for formatting error messages.
+     * {@return the resources to use for formatting error messages}.
      */
     final Errors errors() {
         return Errors.getResources(store.getLocale());
     }
 
     /**
-     * Returns the GeoTIFF-specific resource for error messages and warnings.
+     * {@return the GeoTIFF-specific resource for error messages and warnings}.
      */
     final Resources resources() {
         return Resources.forLocale(store.getLocale());
     }
-
-    /**
-     * Returns the object to use for parsing and formatting dates.
-     */
-    final DateFormat getDateFormat() {
-        if (dateFormat == null) {
-            dateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", LOCALE);
-            if (TIMEZONE != null) {
-                dateFormat.setTimeZone(TIMEZONE);
-            }
-        }
-        return dateFormat;
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index c24065bca3..a0540b2b90 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -38,10 +38,13 @@ import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.geotiff.internal.Resources;
-import org.apache.sis.storage.geotiff.internal.Predictor;
-import org.apache.sis.storage.geotiff.internal.Compression;
-import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.geotiff.base.Tags;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.geotiff.base.Predictor;
+import org.apache.sis.storage.geotiff.base.Compression;
+import org.apache.sis.storage.geotiff.reader.Type;
+import org.apache.sis.storage.geotiff.reader.GridGeometryBuilder;
+import org.apache.sis.storage.geotiff.reader.ImageMetadataBuilder;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -50,6 +53,7 @@ import org.apache.sis.coverage.grid.j2d.ColorModelFactory;
 import org.apache.sis.coverage.grid.j2d.SampleModelFactory;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.internal.UnmodifiableArrayList;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.internal.Strings;
@@ -460,7 +464,7 @@ final class ImageFileDirectory extends DataCube {
     /**
      * Returns the image index used in the default identifier.
      */
-    final String getImageIndex() {
+    private String getImageIndex() {
         return String.valueOf(index + 1);
     }
 
@@ -1007,8 +1011,8 @@ final class ImageFileDirectory extends DataCube {
              */
             case TAG_DATE_TIME: {
                 for (final String value : type.readAsStrings(input(), count, 
encoding())) {
-                    
metadata.addCitationDate(reader.getDateFormat().parse(value),
-                            DateType.CREATION, MetadataBuilder.Scope.RESOURCE);
+                    
metadata.addCitationDate(reader.store.getDateFormat().parse(value),
+                            DateType.CREATION, 
ImageMetadataBuilder.Scope.RESOURCE);
                 }
                 break;
             }
@@ -1126,7 +1130,7 @@ final class ImageFileDirectory extends DataCube {
 
             case Tags.GEO_METADATA:
             case Tags.GDAL_METADATA: {
-                metadata.addXML(new XMLMetadata(reader, type, count, tag));
+                metadata.addXML(reader.readXML(type, count, tag));
                 break;
             }
             case Tags.GDAL_NODATA: {
@@ -1373,6 +1377,11 @@ final class ImageFileDirectory extends DataCube {
             return super.createMetadata();
         }
         this.metadata = null;     // Clear now in case an exception happens.
+        getIdentifier().ifPresent((id) -> {
+            if (!getImageIndex().equals(id.tip().toString())) {
+                metadata.addTitle(id.toString());
+            }
+        });
         /*
          * Add information about sample dimensions.
          *
@@ -1404,10 +1413,21 @@ final class ImageFileDirectory extends DataCube {
             }
             referencing.completeMetadata(gridGeometry, metadata);
         }
+        /*
+         * Add information about the file format.
+         *
+         * Destination: metadata/identificationInfo/resourceFormat
+         */
+        if (reader.store.hidden) {
+            reader.store.setFormatInfo(metadata);   // Should be before 
`addCompression(…)`.
+        }
+        if (compression != null) {
+            
metadata.addCompression(CharSequences.upperCaseToSentence(compression.name()));
+        }
         /*
          * End of metadata construction from TIFF tags.
          */
-        metadata.finish(this, listeners);
+        metadata.finish(reader.store, listeners);
         final DefaultMetadata md = metadata.build();
         if (isIndexValid) {
             final Metadata c = reader.store.customizer.customize(index, md);
@@ -1458,7 +1478,7 @@ final class ImageFileDirectory extends DataCube {
         synchronized (getSynchronizationLock()) {
             if (gridGeometry == null) {
                 if (referencing != null) try {
-                    gridGeometry = referencing.build(reader, imageWidth, 
imageHeight);
+                    gridGeometry = referencing.build(reader.store.listeners(), 
imageWidth, imageHeight);
                 } catch (FactoryException e) {
                     throw new 
DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1,
 filename()), e);
                 } else {
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/NativeMetadata.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/NativeMetadata.java
index 8bb2de1fed..bbbaeaadb3 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/NativeMetadata.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/NativeMetadata.java
@@ -30,8 +30,13 @@ import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.io.stream.ChannelDataInput;
-import org.apache.sis.storage.geotiff.internal.Compression;
-import org.apache.sis.storage.geotiff.internal.Predictor;
+import org.apache.sis.storage.geotiff.base.Compression;
+import org.apache.sis.storage.geotiff.base.Predictor;
+import org.apache.sis.storage.geotiff.base.GeoKeys;
+import org.apache.sis.storage.geotiff.base.Tags;
+import org.apache.sis.storage.geotiff.reader.Type;
+import org.apache.sis.storage.geotiff.reader.GeoKeysLoader;
+import org.apache.sis.storage.geotiff.reader.XMLMetadata;
 
 import static java.lang.Math.addExact;
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
@@ -62,12 +67,12 @@ final class NativeMetadata extends GeoKeysLoader {
      * Column for the name associated to the tag.
      * Value may be null if the name is unknown.
      */
-    static final TableColumn<CharSequence> NAME = TableColumn.NAME;
+    private static final TableColumn<CharSequence> NAME = TableColumn.NAME;
 
     /**
      * Column for the value associated to the tag.
      */
-    static final TableColumn<Object> VALUE = TableColumn.VALUE;
+    private static final TableColumn<Object> VALUE = TableColumn.VALUE;
 
     /**
      * The stream from which to read the data.
@@ -174,7 +179,7 @@ final class NativeMetadata extends GeoKeysLoader {
                             }
                             case Tags.GDAL_METADATA:
                             case Tags.GEO_METADATA: {
-                                children = new XMLMetadata(reader, type, 
count, tag);
+                                children = reader.readXML(type, count, tag);
                                 if (children.isEmpty()) {
                                     // Fallback on showing array of numerical 
values.
                                     value = type.readAsVector(input, count);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
index 4901c84c7d..3dda195317 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
@@ -30,7 +30,10 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.InternalDataStoreException;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.geotiff.base.Tags;
+import org.apache.sis.storage.geotiff.reader.Type;
+import org.apache.sis.storage.geotiff.reader.XMLMetadata;
 import org.apache.sis.util.iso.DefaultNameFactory;
 import org.apache.sis.util.resources.Errors;
 
@@ -51,7 +54,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class Reader extends GeoTIFF {
+final class Reader extends IOBase {
     /**
      * The stream from which to read the data.
      */
@@ -190,7 +193,7 @@ final class Reader extends GeoTIFF {
             }
         }
         // Do not invoke this.errors() yet because GeoTiffStore construction 
may not be finished. Owner.error() is okay.
-        throw new 
DataStoreContentException(store.errors().getString(Errors.Keys.UnexpectedFileFormat_2,
 "TIFF", input.filename));
+        throw new 
DataStoreContentException(errors().getString(Errors.Keys.UnexpectedFileFormat_2,
 "TIFF", input.filename));
     }
 
     /**
@@ -443,6 +446,19 @@ final class Reader extends GeoTIFF {
         return image;
     }
 
+    /**
+     * Reads metadata that embedded in some TIFF files as XML document.
+     *
+     * @param  type       type of the metadata tag to read.
+     * @param  count      number of bytes or characters in the value to read.
+     * @param  tag        the tag where the metadata was stored.
+     * @return the metadata embedded in a XML document.
+     * @throws IOException if an error occurred while reading the TIFF tag 
content.
+     */
+    final XMLMetadata readXML(final Type type, final long count, final short 
tag) throws IOException {
+        return new XMLMetadata(input, store.encoding, store.listeners(), type, 
count, tag);
+    }
+
     /**
      * Logs a warning about a tag that cannot be read, but does not interrupt 
the TIFF reading.
      *
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 91b733ceeb..d8dc3aca6b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -48,6 +48,10 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.math.Fraction;
+import org.apache.sis.storage.geotiff.writer.TagValue;
+import org.apache.sis.storage.geotiff.writer.TileMatrix;
+import org.apache.sis.storage.geotiff.writer.GeoEncoder;
+import org.apache.sis.storage.geotiff.writer.ReformattedImage;
 
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
@@ -69,7 +73,7 @@ import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
  * @author  Erwan Roussel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class Writer extends GeoTIFF implements Flushable {
+final class Writer extends IOBase implements Flushable {
     /**
      * BigTIFF code for unsigned 64-bits integer type.
      *
@@ -147,7 +151,7 @@ final class Writer extends GeoTIFF implements Flushable {
      * Write operations for tag having data too large for fitting inside a IFD 
tag entry.
      * The writing of those data need to be delayed somewhere after the 
sequence of entries.
      */
-    private final Queue<TagValueWriter> largeTagData = new ArrayDeque<>();
+    private final Queue<TagValue> largeTagData = new ArrayDeque<>();
 
     /**
      * Number of TIFF tag entries in the image being written.
@@ -238,7 +242,7 @@ final class Writer extends GeoTIFF implements Flushable {
      * {@return the processor to use for reformatting the image before to 
write it}.
      * The processor is created only when this method is first invoked.
      */
-    final ImageProcessor processor() {
+    private ImageProcessor processor() {
         if (processor == null) {
             processor = new ImageProcessor();
         }
@@ -262,9 +266,9 @@ final class Writer extends GeoTIFF implements Flushable {
     public final long append(final RenderedImage image, final GridGeometry 
grid, final Metadata metadata)
             throws IOException, DataStoreException
     {
-        final TileMatrixWriter tiles;
+        final TileMatrix tiles;
         try {
-            tiles = writeImageFileDirectory(new ReformattedImage(this, image), 
grid, metadata, false);
+            tiles = writeImageFileDirectory(new ReformattedImage(image, 
this::processor), grid, metadata, false);
         } finally {
             largeTagData.clear();       // For making sure that there is no 
memory retention.
         }
@@ -309,7 +313,7 @@ final class Writer extends GeoTIFF implements Flushable {
      * @throws DataStoreException if the given {@code image} has a property
      *         which is not supported by TIFF specification or by this writer.
      */
-    private TileMatrixWriter writeImageFileDirectory(final ReformattedImage 
image, final GridGeometry grid, final Metadata metadata,
+    private TileMatrix writeImageFileDirectory(final ReformattedImage image, 
final GridGeometry grid, final Metadata metadata,
             final boolean overview) throws IOException, DataStoreException
     {
         /*
@@ -342,14 +346,14 @@ final class Writer extends GeoTIFF implements Flushable {
         final double[][] statistics = image.statistics(numBands);
         final  short[][] shortStats = toShorts(statistics, sampleFormat);
         final MetadataFetcher<String> mf = new 
MetadataFetcher<>(store.dataLocale) {
-            @Override protected String parseDate(final Date date) {
-                return getDateFormat().format(date);
+            @Override protected String convertDate(final Date date) {
+                return store.getDateFormat().format(date);
             }
         };
         mf.accept(metadata);
-        GeoKeysWriter geoKeys = null;
+        GeoEncoder geoKeys = null;
         if (grid != null && grid.isDefined(GridGeometry.GRID_TO_CRS)) try {
-            geoKeys = new GeoKeysWriter(store);
+            geoKeys = new GeoEncoder(store.listeners());
             geoKeys.write(grid, mf);
         } catch (FactoryException | IncommensurableException | 
RuntimeException e) {
             throw new DataStoreReferencingException(e);
@@ -395,7 +399,7 @@ final class Writer extends GeoTIFF implements Flushable {
         if (colorInterpretation == PHOTOMETRIC_INTERPRETATION_PALETTE_COLOR) {
             writeColorPalette((IndexColorModel) 
image.visibleBands.getColorModel(), 1L << bitsPerSample[0]);
         }
-        final var tiling = new TileMatrixWriter(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD);
+        final var tiling = new TileMatrix(image.visibleBands, numPlanes, 
bitsPerSample, offsetIFD);
         writeTag((short) TAG_TILE_WIDTH,  (short) TIFFTag.TIFF_LONG, 
tiling.tileWidth);
         writeTag((short) TAG_TILE_LENGTH, (short) TIFFTag.TIFF_LONG, 
tiling.tileHeight);
         tiling.offsetsTag = writeTag((short) TAG_TILE_OFFSETS, tiling.offsets);
@@ -416,11 +420,9 @@ final class Writer extends GeoTIFF implements Flushable {
         tagCountWriter.setAsLong(numberOfTags);
         writeOrQueue(tagCountWriter);
         nextIFD = writeOffset(0);
-        for (final TagValueWriter tag : largeTagData) {
-            final UpdatableWrite<?> offset = tag.offset;
-            offset.setAsLong(output.getStreamPosition());
-            writeOrQueue(offset);
-            tag.write(output);
+        for (final TagValue tag : largeTagData) {
+            UpdatableWrite<?> offset = tag.writeHere(output);
+            if (offset != null) deferredWrites.add(offset);
         }
         return tiling;
     }
@@ -509,7 +511,7 @@ final class Writer extends GeoTIFF implements Flushable {
             buffer.putInt((int) count);
             return Integer.BYTES;
         } else {
-            throw new 
ArithmeticException(store.errors().getString(Errors.Keys.IntegerOverflow_1, 
Integer.SIZE));
+            throw new 
ArithmeticException(errors().getString(Errors.Keys.IntegerOverflow_1, 
Integer.SIZE));
         }
     }
 
@@ -522,14 +524,13 @@ final class Writer extends GeoTIFF implements Flushable {
      * @throws IOException if an error occurred while writing to the output.
      * @throws ArithmeticException if the count is too large for the TIFF 
format in use.
      */
-    private TagValueWriter writeLargeTag(final short tag, final short type, 
final long count, final TagValueWriter deferred) throws IOException {
+    private TagValue writeLargeTag(final short tag, final short type, final 
long count, final TagValue deferred) throws IOException {
         final long r = writeTagHeader(tag, type, count) - TYPE_SIZES[type] * 
count;
         if (r >= 0) {
-            deferred.offset = UpdatableWrite.of(output);        // Record only 
the position.
-            deferred.write(output);
+            deferred.markAndWrite(output);
             output.repeat(r, (byte) 0);
         } else {
-            deferred.offset = writeOffset(0);
+            deferred.mark(writeOffset(0));
             largeTagData.add(deferred);
         }
         return deferred;
@@ -544,8 +545,8 @@ final class Writer extends GeoTIFF implements Flushable {
      */
     private void writeColorPalette(final IndexColorModel cm, final long count) 
throws IOException {
         final int numBands = 3;
-        writeLargeTag((short) TAG_COLOR_MAP, (short) TIFFTag.TIFF_SHORT, count 
* numBands, new TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        writeLargeTag((short) TAG_COLOR_MAP, (short) TIFFTag.TIFF_SHORT, count 
* numBands, new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 final int n = (int) Math.min(cm.getMapSize(), count);
                 for (int band=0; band < numBands; band++) {
                     for (int i=0; i<n; i++) {
@@ -595,8 +596,8 @@ final class Writer extends GeoTIFF implements Flushable {
             }
         }
         if (count != 0) {
-            writeLargeTag(tag, (short) TIFFTag.TIFF_ASCII, count, new 
TagValueWriter() {
-                @Override void write(final ChannelDataOutput output) throws 
IOException {
+            writeLargeTag(tag, (short) TIFFTag.TIFF_ASCII, count, new 
TagValue() {
+                @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                     for (final byte[] c : chars) {
                         if (c != null) {
                             output.write(c);
@@ -623,8 +624,8 @@ final class Writer extends GeoTIFF implements Flushable {
         if (value == null) {
             return;
         }
-        writeLargeTag(tag, (short) TIFFTag.TIFF_RATIONAL, 1, new 
TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        writeLargeTag(tag, (short) TIFFTag.TIFF_RATIONAL, 1, new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 output.writeInt(value.numerator);
                 output.writeInt(value.denominator);
             }
@@ -641,12 +642,12 @@ final class Writer extends GeoTIFF implements Flushable {
      * @return a handler for rewriting the data if the array content changes.
      * @throws IOException if an error occurred while writing to the output.
      */
-    private TagValueWriter writeTag(final short tag, final short type, final 
double[] values) throws IOException {
+    private TagValue writeTag(final short tag, final short type, final 
double[] values) throws IOException {
         if (values == null || values.length == 0) {
             return null;
         }
-        return writeLargeTag(tag, type, values.length, new TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        return writeLargeTag(tag, type, values.length, new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 switch (type) {
                     default: throw new AssertionError(type);
                     case TIFFTag.TIFF_DOUBLE: output.writeDoubles(values); 
break;
@@ -670,13 +671,13 @@ final class Writer extends GeoTIFF implements Flushable {
      * @return a handler for rewriting the data if the array content changes.
      * @throws IOException if an error occurred while writing to the output.
      */
-    private TagValueWriter writeTag(final short tag, final long[] values) 
throws IOException {
+    private TagValue writeTag(final short tag, final long[] values) throws 
IOException {
         if (values == null || values.length == 0) {
             return null;
         }
         final short type = isBigTIFF ? TIFF_ULONG : TIFFTag.TIFF_LONG;
-        return writeLargeTag(tag, type, values.length, new TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        return writeLargeTag(tag, type, values.length, new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 switch (type) {
                     default: throw new AssertionError(type);
                     case TIFF_ULONG: output.writeLongs(values); break;
@@ -699,12 +700,12 @@ final class Writer extends GeoTIFF implements Flushable {
      * @return a handler for rewriting the data if the array content changes.
      * @throws IOException if an error occurred while writing to the output.
      */
-    private TagValueWriter writeTag(final short tag, final short[] values) 
throws IOException {
+    private TagValue writeTag(final short tag, final short[] values) throws 
IOException {
         if (values == null || values.length == 0) {
             return null;
         }
-        return writeLargeTag(tag, (short) TIFFTag.TIFF_SHORT, values.length, 
new TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        return writeLargeTag(tag, (short) TIFFTag.TIFF_SHORT, values.length, 
new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 output.writeShorts(values);
             }
         });
@@ -720,12 +721,12 @@ final class Writer extends GeoTIFF implements Flushable {
      * @return a handler for rewriting the data if the array content changes.
      * @throws IOException if an error occurred while writing to the output.
      */
-    private TagValueWriter writeTag(final short tag, final short type, final 
int[] values) throws IOException {
+    private TagValue writeTag(final short tag, final short type, final int[] 
values) throws IOException {
         if (values == null || values.length == 0) {
             return null;
         }
-        return writeLargeTag(tag, type, values.length, new TagValueWriter() {
-            @Override void write(final ChannelDataOutput output) throws 
IOException {
+        return writeLargeTag(tag, type, values.length, new TagValue() {
+            @Override protected void write(final ChannelDataOutput output) 
throws IOException {
                 switch (type) {
                     default: throw new AssertionError(type);
                     case TIFFTag.TIFF_LONG: output.writeInts(values); break;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Compression.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Compression.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java
index f11c10881a..17f0b0ab49 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Compression.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Compression.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoCodes.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoCodes.java
similarity index 74%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoCodes.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoCodes.java
index 80d3e06ba0..e99029508b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoCodes.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoCodes.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 
 /**
@@ -24,7 +24,7 @@ package org.apache.sis.storage.geotiff;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class GeoCodes {
+public final class GeoCodes {
     /**
      * Do not allow instantiation of this class.
      */
@@ -45,7 +45,7 @@ final class GeoCodes {
      * An alternative code for {@link #undefined} found in some GeoTIFF file.
      * This is not a standard value. This is used only in some methods 
implemented defensively.
      */
-    static final short missing = -1;
+    public static final short missing = -1;
 
     /*
      * 6.3.1.1 Model Type Codes
@@ -85,4 +85,31 @@ final class GeoCodes {
      * This is handled as a special case for distinguishing between variants.
      */
     public static final short PolarStereographic = 15;
+
+    /**
+     * Number of GeoTIFF keys.
+     * This value is verified by the {@code GeoKeysTest.verifyNumKeys()} test.
+     *
+     * <p>This field should be part of {@link GeoKeys}, but is declared here 
because we
+     * need to avoid public constants that are not GeoKey names in {@code 
GeoKeys}.</p>
+     */
+    public static final int NUM_GEOKEYS = 46;
+
+    /**
+     * Number of GeoTIFF key associated to values of type {@code double}.
+     *
+     * <p>This field should be part of {@link GeoKeys}, but is declared here 
because we
+     * need to avoid public constants that are not GeoKey names in {@code 
GeoKeys}.</p>
+     */
+    public static final int NUM_DOUBLE_GEOKEYS = 25;
+
+    /**
+     * Number of {@code short} values in each GeoKey entry.
+     */
+    public static final int ENTRY_LENGTH = 4;
+
+    /**
+     * The character used as a separator in {@link String} multi-values.
+     */
+    public static final char STRING_SEPARATOR = '|';
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeys.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoKeys.java
similarity index 93%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeys.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoKeys.java
index 2762793adc..6abbd6919a 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeys.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/GeoKeys.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import java.lang.reflect.Field;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -43,7 +43,7 @@ import org.opengis.referencing.operation.MathTransform;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class GeoKeys {
+public final class GeoKeys {
     /**
      * Do not allow instantiation of this class.
      */
@@ -105,24 +105,11 @@ final class GeoKeys {
     /** For user-defined CRS.  */ public static final short VerticalDatum      
    = 4098;
     /** For vertical axis.     */ public static final short VerticalUnits      
    = 4099;
 
-    /**
-     * Number of keys. Because keys cannot be repeated, this is the maximal
-     * number of entries that {@link GeoKeysWriter#keyDirectory} can contain.
-     * This value is verified by the {@code GeoKeysTest.verifyNumKeys()}.
-     */
-    static final int NUM_KEYS = 46;
-
-    /**
-     * Number of parameters that are of type {@code double}.
-     * This is the maximal length of {@link GeoKeysWriter#doubleParams}.
-     */
-    static final int NUM_DOUBLES = 25;
-
     /**
      * Returns the name of the given key. Implementation of this method is 
inefficient,
      * but it should rarely be invoked (mostly for formatting error messages).
      */
-    static String name(final short key) {
+    public static String name(final short key) {
         try {
             for (final Field field : GeoKeys.class.getFields()) {
                 if (field.getType() == Short.TYPE) {
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Predictor.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
similarity index 97%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Predictor.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
index c97846e2be..834169505e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Predictor.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Predictor.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources.java
index 832cc46ce8..8fc671e7f4 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 import java.io.InputStream;
 import java.util.Locale;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources.properties
similarity index 100%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources.properties
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources.properties
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_en.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_en.java
similarity index 95%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_en.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_en.java
index a09db44680..d92caeb33b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_en.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_en.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 
 /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_fr.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_fr.java
similarity index 95%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_fr.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_fr.java
index 28ed9111cc..17693ea012 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_fr.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_fr.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 
 /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_fr.properties
similarity index 100%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/Resources_fr.properties
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Resources_fr.properties
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Tags.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
similarity index 96%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Tags.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
index b4c38827e7..f301a7a166 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Tags.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/Tags.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import java.lang.reflect.Field;
 import java.util.function.Supplier;
@@ -34,7 +34,7 @@ import javax.imageio.plugins.tiff.TIFFTagSet;
  *
  * @author  Johann Sorel (Geomatys)
  */
-final class Tags {
+public final class Tags {
     /**
      * XML packet containing metadata such as descriptions, titles, keywords, 
author and copyright information.
      *
@@ -92,10 +92,10 @@ final class Tags {
     }
 
     /**
-     * Returns the name of the given tag.
+     * {@return the name of the given tag}.
      * This method should be rarely invoked (mostly for formatting error 
messages).
      */
-    static String name(final short tag) {
+    public static String name(final short tag) {
         final int ti = Short.toUnsignedInt(tag);
         for (final Supplier<TIFFTagSet> s : TAG_SETS) {
             final TIFFTag t = s.get().getTag(ti);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/UnitKey.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/UnitKey.java
similarity index 95%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/UnitKey.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/UnitKey.java
index 4aacfb1064..b8c4e16c3c 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/UnitKey.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/UnitKey.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import javax.measure.Unit;
 import org.apache.sis.measure.Units;
@@ -27,7 +27,7 @@ import org.apache.sis.util.Workaround;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-enum UnitKey {
+public enum UnitKey {
     /**
      * Linear unit in geodetic CRS. Used for
      * the axes in user-defined geocentric Cartesian CRSs,
@@ -84,17 +84,17 @@ enum UnitKey {
     /**
      * The {@link GeoKeys} for a unit of measurement defined by an EPSG code, 
or 0 if none.
      */
-    final short codeKey;
+    public final short codeKey;
 
     /**
      * The {@link GeoKeys} for a unit of measurement defined by a scale 
applied on a base unit, or 0 if none.
      */
-    final short scaleKey;
+    public final short scaleKey;
 
     /**
      * Whether the unit may be associated to coordinate system axes.
      */
-    final boolean isAxis;
+    public final boolean isAxis;
 
     /**
      * Whether the key accepts linear, angular or scalar units.
@@ -129,7 +129,7 @@ enum UnitKey {
      * @return the unit of measurement of the map projection parameter, or 
{@link #LINEAR} or {@link #NULL}
      *         if the given parameter is not a map projection parameter.
      */
-    static UnitKey ofProjectionParameter(final short key) {
+    public static UnitKey ofProjectionParameter(final short key) {
         switch (key) {
             case GeoKeys.SemiMajorAxis:
             case GeoKeys.SemiMinorAxis:      return LINEAR;
@@ -160,7 +160,7 @@ enum UnitKey {
      * @param  unit  unit of measurement of an axis of a geodetic CRS.
      * @return the key to use for the specified unit, or {@code null} if none.
      */
-    UnitKey validate(final Unit<?> unit) {
+    public UnitKey validate(final Unit<?> unit) {
         if ((linear  && Units.isLinear (unit)) ||
             (angular && Units.isAngular(unit)) ||
             (scalar  && Units.isScale  (unit)))
@@ -177,7 +177,7 @@ enum UnitKey {
     /**
      * {@return the default unit of measurement, or {@code null} if none}.
      */
-    Unit<?> defaultUnit() {
+    public Unit<?> defaultUnit() {
         if (linear)  return Units.METRE;
         if (angular) return Units.DEGREE;
         if (scalar)  return Units.UNITY;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/package-info.java
similarity index 89%
copy from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
copy to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/package-info.java
index 156321c438..d789ea14da 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/base/package-info.java
@@ -16,7 +16,7 @@
  */
 
 /**
- * Utility classes for the implementation of GeoTIFF reader and writer.
+ * Shared classes for the implementation of GeoTIFF reader and writer.
  *
  * <STRONG>Do not use!</STRONG>
  *
@@ -25,4 +25,4 @@
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/CompressionChannel.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/CompressionChannel.java
index 08d03c0f2f..87ff92d9fd 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/CompressionChannel.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/CompressionChannel.java
@@ -22,7 +22,7 @@ import java.nio.ByteBuffer;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.storage.StorageConnector;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.storage.event.StoreListeners;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
index 3adadb067f..25dfb9f396 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/Inflater.java
@@ -25,9 +25,9 @@ import org.apache.sis.math.MathFunctions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.storage.UnsupportedEncodingException;
-import org.apache.sis.storage.geotiff.internal.Compression;
-import org.apache.sis.storage.geotiff.internal.Predictor;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Compression;
+import org.apache.sis.storage.geotiff.base.Predictor;
+import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.storage.event.StoreListeners;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/LZW.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/LZW.java
index 7e309ec153..7524ffa147 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/LZW.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/LZW.java
@@ -20,7 +20,7 @@ import java.util.Arrays;
 import java.io.IOException;
 import java.io.EOFException;
 import java.nio.ByteBuffer;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.storage.event.StoreListeners;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
index c110210883..7624f5e269 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/inflater/PredictorChannel.java
@@ -19,7 +19,7 @@ package org.apache.sis.storage.geotiff.inflater;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import org.apache.sis.util.ArraysExt;
-import org.apache.sis.storage.geotiff.internal.Predictor;
+import org.apache.sis.storage.geotiff.base.Predictor;
 import org.apache.sis.pending.jdk.JDK17;
 
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/CRSBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
similarity index 97%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/CRSBuilder.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
index e3643d7088..1350c57d0b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/CRSBuilder.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.Arrays;
 import java.util.Set;
@@ -82,7 +82,12 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 import org.apache.sis.metadata.iso.citation.Citations;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.geotiff.GeoTiffStore;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.geotiff.base.GeoCodes;
+import org.apache.sis.storage.geotiff.base.GeoKeys;
+import org.apache.sis.storage.geotiff.base.UnitKey;
 
 import static org.apache.sis.util.Utilities.equalsIgnoreMetadata;
 
@@ -97,7 +102,7 @@ import static 
org.apache.sis.util.Utilities.equalsIgnoreMetadata;
  * @see GeoKeys
  * @see GeoKeysLoader
  */
-final class CRSBuilder extends ReferencingFactoryContainer {
+public final class CRSBuilder extends ReferencingFactoryContainer {
     /**
      * Index where to store the name of the geodetic CRS, the datum, the 
ellipsoid and the prime meridian.
      * The GeoTIFF specification has only one key, {@link 
GeoKeys#GeodeticCitation}, for the geographic CRS
@@ -134,10 +139,9 @@ final class CRSBuilder extends ReferencingFactoryContainer 
{
     private static final int MIN_KEY_LENGTH = 5;
 
     /**
-     * The reader for which we will create coordinate reference systems.
-     * This is used for reporting warnings.
+     * The listeners where to report warnings.
      */
-    private final Reader reader;
+    private final StoreListeners listeners;
 
     /**
      * Version of the set of keys declared in the {@code GeoKeyDirectory} 
header.
@@ -189,16 +193,23 @@ final class CRSBuilder extends 
ReferencingFactoryContainer {
     /**
      * Creates a new builder of coordinate reference systems.
      *
-     * @param reader  where to report warnings if any.
+     * @param  listeners  the listeners where to report warnings.
      */
-    CRSBuilder(final Reader reader) {
-        this.reader = reader;
+    public CRSBuilder(final StoreListeners listeners) {
+        this.listeners = listeners;
         geoKeys = new HashMap<>(32);
         missingGeoKeys = new HashSet<>();
     }
 
     /**
      * Reports a warning with a message built from the given resource keys and 
arguments.
+     * Note that the record will not necessarily be sent to the logging 
framework.
+     * If the user has registered at least one listener, then the record will 
be sent to the listeners instead.
+     *
+     * <p>This method sets the {@linkplain 
LogRecord#setSourceClassName(String) source class name} and
+     * {@linkplain LogRecord#setSourceMethodName(String) source method name} 
to hard-coded values.
+     * Those values assume that the warnings occurred indirectly from a call 
to {@link #getMetadata()}
+     * in this class. We do not report private classes or methods as the 
source of warnings.</p>
      *
      * @param  key   one of the {@link Resources.Keys} constants.
      * @param  args  arguments for the log message.
@@ -207,8 +218,11 @@ final class CRSBuilder extends ReferencingFactoryContainer 
{
      * @see GeoKeysLoader#warning(short, Object...)
      */
     final void warning(final short key, final Object... args) {
-        final LogRecord r = reader.resources().getLogRecord(Level.WARNING, 
key, args);
-        reader.store.warning(r);
+        LogRecord record = 
Resources.forLocale(listeners.getLocale()).getLogRecord(Level.WARNING, key, 
args);
+        // Logger name will be set by listeners.warning(record).
+        record.setSourceClassName(GeoTiffStore.class.getName());
+        record.setSourceMethodName("getMetadata");
+        listeners.warning(record);
     }
 
     /**
@@ -456,7 +470,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
                 try {
                     expected = Integer.parseInt(id.getCode());
                 } catch (NumberFormatException e) {
-                    reader.store.listeners().warning(e);            // Should 
not happen.
+                    listeners.warning(e);               // Should not happen.
                     return;
                 }
                 if (code != expected) {
@@ -578,7 +592,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
         if (epsg != null) try {
             return getCSAuthorityFactory().createCartesianCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
-            reader.store.listeners().warning(e);
+            listeners.warning(e);
         }
         return (CartesianCS) CoordinateSystems.replaceLinearUnit(cs, unit);
     }
@@ -596,7 +610,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
         if (epsg != null) try {
             return 
getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
         } catch (NoSuchAuthorityCodeException e) {
-            reader.store.listeners().warning(e);
+            listeners.warning(e);
         }
         return (EllipsoidalCS) CoordinateSystems.replaceAngularUnit(cs, unit);
     }
@@ -950,7 +964,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
      */
     static String[] splitName(final String name) {
         final String[] names = new String[GCRS + 1];
-        final String[] components = (String[]) CharSequences.split(name, 
GeoKeysLoader.SEPARATOR);
+        final String[] components = (String[]) CharSequences.split(name, 
GeoCodes.STRING_SEPARATOR);
         switch (components.length) {
             case 0: break;
             case 1: names[GCRS] = name; break;
@@ -1404,8 +1418,8 @@ final class CRSBuilder extends 
ReferencingFactoryContainer {
                         String paramName = 
toNames.get(Short.toUnsignedInt(key));
                         if (paramName == null) {
                             paramName = GeoKeys.name(key);
-                            throw new 
ParameterNotFoundException(reader.errors().getString(
-                                    Errors.Keys.UnexpectedParameter_1, 
paramName), paramName);
+                            throw new 
ParameterNotFoundException(Errors.getResources(listeners.getLocale())
+                                        
.getString(Errors.Keys.UnexpectedParameter_1, paramName), paramName);
                         }
                         final Number value  = paramValues.get(key);
                         final Number actual = 
paramValues.putIfAbsent(paramName, value);
@@ -1574,7 +1588,7 @@ final class CRSBuilder extends 
ReferencingFactoryContainer {
     @Override
     public final String toString() {
         final StringBuilder buffer = new StringBuilder("GeoTIFF keys 
").append(majorRevision).append('.')
-                .append(minorRevision).append(" in 
").append(reader.input.filename).append(System.lineSeparator());
+                .append(minorRevision).append(" in 
").append(listeners.getSourceName()).append(System.lineSeparator());
         final TableAppender table = new TableAppender(buffer, " ");
         for (Map.Entry<Short,Object> entry : geoKeys.entrySet()) {
             final short key = entry.getKey();
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysLoader.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GeoKeysLoader.java
similarity index 94%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysLoader.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GeoKeysLoader.java
index 2a62e2a816..869a69c8ba 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysLoader.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GeoKeysLoader.java
@@ -14,12 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.Map;
 import org.apache.sis.math.Vector;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.GeoKeys;
+import org.apache.sis.storage.geotiff.base.GeoCodes;
+import org.apache.sis.storage.geotiff.base.Resources;
 
 import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
 
@@ -67,17 +69,7 @@ import static javax.imageio.plugins.tiff.GeoTIFFTagSet.*;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-class GeoKeysLoader {
-    /**
-     * Number of {@code short} values in each GeoKey entry.
-     */
-    static final int ENTRY_LENGTH = 4;
-
-    /**
-     * The character used as a separator in {@link String} multi-values.
-     */
-    static final char SEPARATOR = '|';
-
+public class GeoKeysLoader {
     /**
      * References the {@link GeoKeys} needed for building the Coordinate 
Reference System.
      * Cannot be null when invoking {@link #load(Map)}.
@@ -96,7 +88,7 @@ class GeoKeysLoader {
      *
      * @see #setAsciiParameters(String[])
      */
-    public String asciiParameters;
+    String asciiParameters;
 
     /**
      * Version of the set of keys declared in the {@code GeoKeyDirectory} 
header.
@@ -114,13 +106,13 @@ class GeoKeysLoader {
      * Creates a new GeoTIFF keys loader. The {@link #keyDirectory}, {@link 
#numericParameters}
      * {@link #asciiParameters} and {@link #logger} fields must be initialized 
by the caller.
      */
-    GeoKeysLoader() {
+    protected GeoKeysLoader() {
     }
 
     /**
      * Sets the value of {@link #asciiParameters} from {@code GeoAsciiParams} 
value.
      */
-    final void setAsciiParameters(final String[] values) {
+    public final void setAsciiParameters(final String[] values) {
         switch (values.length) {
             case 0:  break;
             case 1:  asciiParameters = values[0]; break;
@@ -134,10 +126,10 @@ class GeoKeysLoader {
      * @param  geoKeys  where to write GeoKeys.
      * @return whether the operation succeed.
      */
-    final boolean load(final Map<Short,Object> geoKeys) {
+    protected final boolean load(final Map<Short,Object> geoKeys) {
         final int numberOfKeys;
         final int directoryLength = keyDirectory.size();
-        if (directoryLength >= ENTRY_LENGTH) {
+        if (directoryLength >= GeoCodes.ENTRY_LENGTH) {
             final int version = keyDirectory.intValue(0);
             if (version != 1) {
                 warning(Resources.Keys.UnsupportedGeoKeyDirectory_1, version);
@@ -156,7 +148,7 @@ class GeoKeysLoader {
          *
          *     (number of key + head) * 4    ---    1 entry = 4 short values.
          */
-        final int expectedLength = (numberOfKeys + 1) * ENTRY_LENGTH;
+        final int expectedLength = (numberOfKeys + 1) * GeoCodes.ENTRY_LENGTH;
         if (directoryLength < expectedLength) {
             warning(Resources.Keys.ListTooShort_3, "GeoKeyDirectory", 
expectedLength, directoryLength);
             return false;
@@ -168,7 +160,7 @@ class GeoKeysLoader {
          * because the CRS creation may use them out of order.
          */
         for (int i=1; i <= numberOfKeys; i++) {
-            final int p = i * ENTRY_LENGTH;
+            final int p = i * GeoCodes.ENTRY_LENGTH;
             final short key       = keyDirectory.shortValue(p);
             final int tagLocation = keyDirectory.intValue(p+1);
             final int count       = keyDirectory.intValue(p+2);
@@ -254,7 +246,7 @@ class GeoKeysLoader {
                         continue;
                     }
                     upper = 
CharSequences.skipTrailingWhitespaces(asciiParameters, valueOffset, upper);
-                    while (upper > valueOffset && asciiParameters.charAt(upper 
- 1) == SEPARATOR) {
+                    while (upper > valueOffset && asciiParameters.charAt(upper 
- 1) == GeoCodes.STRING_SEPARATOR) {
                         upper--;    // Skip trailing pipe, interpreted as 
C/C++ NUL character.
                     }
                     // Use String.trim() for skipping C/C++ NUL character in 
addition of whitespaces.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
similarity index 93%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
index 154496ca06..0afa81fa06 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/GridGeometryBuilder.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.NoSuchElementException;
 import org.opengis.util.FactoryException;
@@ -33,7 +33,8 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import 
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
 import org.apache.sis.storage.base.MetadataBuilder;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.util.internal.DoubleDouble;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -74,7 +75,7 @@ import org.apache.sis.math.Vector;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class GridGeometryBuilder extends GeoKeysLoader {
+public final class GridGeometryBuilder extends GeoKeysLoader {
 
     
////////////////////////////////////////////////////////////////////////////////////////
     ////                                                                       
         ////
@@ -164,21 +165,21 @@ final class GridGeometryBuilder extends GeoKeysLoader {
 
     /**
      * Suggested value for a general description of the transformation form 
grid coordinates to "real world" coordinates.
-     * This information is obtained as a side-effect of {@link #build(Reader, 
long, long)} call.
+     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long)} call.
      */
     private String description;
 
     /**
      * {@code POINT} if {@link GeoKeys#RasterType} is {@link 
GeoCodes#RasterPixelIsPoint},
      * {@code AREA} if it is {@link GeoCodes#RasterPixelIsArea}, or null if 
unspecified.
-     * This information is obtained as a side-effect of {@link #build(Reader, 
long, long)} call.
+     * This information is obtained as a side-effect of {@link 
#build(StoreListeners, long, long)} call.
      */
     private CellGeometry cellGeometry;
 
     /**
      * Creates a new builder.
      */
-    GridGeometryBuilder() {
+    public GridGeometryBuilder() {
     }
 
     /**
@@ -249,16 +250,17 @@ final class GridGeometryBuilder extends GeoKeysLoader {
      * After this method call (if successful), the returned value is 
guaranteed non-null
      * and can be used as a flag for determining that the build has been 
completed.
      *
-     * @param  width   the image width in pixels.
-     * @param  height  the image height in pixels.
+     * @param  listeners  the listeners where to report warnings.
+     * @param  width      the image width in pixels.
+     * @param  height     the image height in pixels.
      * @return the grid geometry, guaranteed non-null.
      * @throws FactoryException if an error occurred while creating a CRS or a 
transform.
      */
     @SuppressWarnings("fallthrough")
-    public GridGeometry build(final Reader reader, final long width, final 
long height) throws FactoryException {
+    public GridGeometry build(final StoreListeners listeners, final long 
width, final long height) throws FactoryException {
         CoordinateReferenceSystem crs = null;
         if (keyDirectory != null) {
-            final CRSBuilder helper = new CRSBuilder(reader);
+            final CRSBuilder helper = new CRSBuilder(listeners);
             try {
                 crs = helper.build(this);
                 description  = helper.description;
@@ -268,10 +270,10 @@ final class GridGeometryBuilder extends GeoKeysLoader {
                 if (e instanceof NoSuchAuthorityCodeException) {
                     key = Resources.Keys.UnknownCRS_1;
                 }
-                
reader.store.listeners().warning(reader.resources().getString(key, 
reader.store.getDisplayName()), e);
+                
listeners.warning(Resources.forLocale(listeners.getLocale()).getString(key, 
listeners.getSourceName()), e);
             } catch (IllegalArgumentException | NoSuchElementException | 
ClassCastException e) {
                 if (!helper.alreadyReported) {
-                    canNotCreate(reader, e);
+                    canNotCreate(listeners, e);
                 }
             }
         }
@@ -310,7 +312,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
                 envelope.setToNaN();
             }
             gridGeometry = new GridGeometry(extent, envelope, 
GridOrientation.HOMOTHETY);
-            canNotCreate(reader, e);
+            canNotCreate(listeners, e);
             /*
              * Note: we catch TransformExceptions because they may be caused 
by erroneous data in the GeoTIFF file,
              * but let FactoryExceptions propagate because they are more 
likely to be a SIS configuration problem.
@@ -329,7 +331,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
      *
      * <h4>Prerequisite</h4>
      * <ul>
-     *   <li>{@link #build(Reader, long, long)} must have been invoked 
successfully before this method.</li>
+     *   <li>{@link #build(StoreListeners, long, long)} must have been invoked 
successfully before this method.</li>
      *   <li>{@link ImageFileDirectory} must have filled its part of metadata 
before to invoke this method.</li>
      * </ul>
      *
@@ -343,7 +345,7 @@ final class GridGeometryBuilder extends GeoKeysLoader {
      *   <li>{@code metadata/referenceSystemInfo}</li>
      * </ul>
      *
-     * @param  gridGeometry  the grid geometry computed by {@link 
#build(Reader, long, long)}.
+     * @param  gridGeometry  the grid geometry computed by {@link 
#build(StoreListeners, long, long)}.
      * @param  metadata      the helper class where to write metadata values.
      * @throws NumberFormatException if a numeric value was stored as a string 
and cannot be parsed.
      */
@@ -373,8 +375,8 @@ final class GridGeometryBuilder extends GeoKeysLoader {
     /**
      * Logs a warning telling that we cannot create a grid geometry for the 
given reason.
      */
-    private static void canNotCreate(final Reader reader, final Exception e) {
-        reader.store.listeners().warning(reader.resources().getString(
-                Resources.Keys.CanNotComputeGridGeometry_1, 
reader.input.filename), e);
+    private static void canNotCreate(final StoreListeners listeners, final 
Exception e) {
+        listeners.warning(Resources.forLocale(listeners.getLocale()).getString(
+                Resources.Keys.CanNotComputeGridGeometry_1, 
listeners.getSourceName()), e);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ImageMetadataBuilder.java
similarity index 85%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ImageMetadataBuilder.java
index aeaae5d361..b36a14a3c2 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageMetadataBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ImageMetadataBuilder.java
@@ -14,16 +14,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.geotiff.internal.Resources;
-import org.apache.sis.storage.geotiff.internal.Compression;
+import org.apache.sis.storage.geotiff.GeoTiffStore;
+import org.apache.sis.storage.geotiff.base.Resources;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.event.StoreListeners;
-import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.measure.Units;
 
@@ -42,7 +41,7 @@ import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class ImageMetadataBuilder extends MetadataBuilder {
+public final class ImageMetadataBuilder extends MetadataBuilder {
     /**
      * The number of pixels per {@link #resolutionUnit} in the image width and 
Height directions,
      * or {@link Double#NaN} if unspecified. Since ISO 19115 does not have 
separated resolution
@@ -78,7 +77,7 @@ final class ImageMetadataBuilder extends MetadataBuilder {
     /**
      * Creates an initially empty metadata builder.
      */
-    ImageMetadataBuilder() {
+    public ImageMetadataBuilder() {
     }
 
     /**
@@ -95,7 +94,7 @@ final class ImageMetadataBuilder extends MetadataBuilder {
      * @return {@code null} on success, or the given value if not recognized.
      */
     @SuppressWarnings("fallthrough")
-    Integer setThreshholding(final int value) {
+    public Integer setThreshholding(final int value) {
         switch (value) {
             default: return value;                      // Cause a warning to 
be reported by the caller.
             case THRESHHOLDING_ORDERED_DITHER: {
@@ -118,15 +117,17 @@ final class ImageMetadataBuilder extends MetadataBuilder {
      * @param  size  the new size.
      * @param  w     {@code true} for setting cell width, or {@code false} for 
setting cell height.
      */
-    void setCellSize(final short size, final boolean w) {
+    public void setCellSize(final short size, final boolean w) {
         if (w) cellWidth = size;
         else  cellHeight = size;
     }
 
     /**
      * Sets the resolution to the maximal of current value and given value.
+     *
+     * @param  r  the resolution.
      */
-    void setResolution(final double r) {
+    public void setResolution(final double r) {
         if (Double.isNaN(resolution) || r > resolution) {
             resolution = r;
         }
@@ -145,7 +146,7 @@ final class ImageMetadataBuilder extends MetadataBuilder {
      * @param  value  the threshholding value.
      * @return {@code null} on success, or the given value if not recognized.
      */
-    Integer setResolutionUnit(final int unit) {
+    public Integer setResolutionUnit(final int unit) {
         switch (unit) {
             case 1:  resolutionUnit = null;             break;
             case 2:  resolutionUnit = Units.INCH;       break;
@@ -159,7 +160,7 @@ final class ImageMetadataBuilder extends MetadataBuilder {
      * Adds metadata in XML format. Those metadata are defined
      * in {@code GEO_METADATA} or {@code GDAL_METADATA} tags.
      */
-    void addXML(final XMLMetadata xml) {
+    public void addXML(final XMLMetadata xml) {
         if (complement == null) {
             complement = xml;
         } else {
@@ -174,25 +175,7 @@ final class ImageMetadataBuilder extends MetadataBuilder {
      *
      * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
      */
-    void finish(final ImageFileDirectory image, final StoreListeners 
listeners) throws DataStoreException {
-        image.getIdentifier().ifPresent((id) -> {
-            if (!image.getImageIndex().equals(id.tip().toString())) {
-                addTitle(id.toString());
-            }
-        });
-        /*
-         * Add information about the file format.
-         *
-         * Destination: metadata/identificationInfo/resourceFormat
-         */
-        final GeoTiffStore store = image.reader.store;
-        if (store.hidden) {
-            store.setFormatInfo(this);       // Should be before 
`addCompression(…)`.
-        }
-        final Compression compression = image.getCompression();
-        if (compression != null) {
-            
addCompression(CharSequences.upperCaseToSentence(compression.name()));
-        }
+    public void finish(final GeoTiffStore store, final StoreListeners 
listeners) throws DataStoreException {
         /*
          * Add the resolution into the metadata. Our current ISO 19115 
implementation restricts
          * the resolution unit to metres, but it may be relaxed in a future 
SIS version.
@@ -238,7 +221,8 @@ final class ImageMetadataBuilder extends MetadataBuilder {
         while (complement != null) try {
             complement = complement.appendTo(this);
         } catch (Exception ex) {
-            
listeners.warning(image.reader.errors().getString(Errors.Keys.CanNotSetPropertyValue_1,
 complement.tag()), ex);
+            listeners.warning(Errors.getResources(listeners.getLocale())
+                    .getString(Errors.Keys.CanNotSetPropertyValue_1, 
complement.tag()), ex);
         }
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Localization.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Localization.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Localization.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Localization.java
index 234279002d..82974b59c1 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Localization.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Localization.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.Arrays;
 import java.util.Set;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ReversedBitsChannel.java
similarity index 93%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ReversedBitsChannel.java
index f8dec2fd9a..2b87e5722c 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReversedBitsChannel.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/ReversedBitsChannel.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -32,7 +32,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Rémi Maréchal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class ReversedBitsChannel implements ReadableByteChannel, 
SeekableByteChannel {
+public final class ReversedBitsChannel implements ReadableByteChannel, 
SeekableByteChannel {
     /**
      * Lookup table for reversing the order of bits in a byte.
      */
@@ -61,8 +61,11 @@ final class ReversedBitsChannel implements 
ReadableByteChannel, SeekableByteChan
      * but with bits order reversed in every byte. The new channel uses a 
temporary buffer of relatively
      * small size because invoking {@link #read(ByteBuffer)} is presumed not 
too costly for this class,
      * and because a new buffer is created for each strip or tile to read.
+     *
+     * @param  input  the channel to wrap.
+     * @return a channel with other of bits reversed.
      */
-    static ChannelDataInput wrap(final ChannelDataInput input) throws 
IOException {
+    public static ChannelDataInput wrap(final ChannelDataInput input) throws 
IOException {
         final var buffer = 
ByteBuffer.allocate(2048).order(input.buffer.order());
         return new ChannelDataInput(input, new ReversedBitsChannel(input), 
buffer);
     }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Type.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Type.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
index 019844d311..b97b204dc6 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Type.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/Type.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.Arrays;
 import java.io.IOException;
@@ -39,7 +39,7 @@ import org.apache.sis.util.resources.Errors;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-enum Type {
+public enum Type {
     /**
      * An 8-bits byte that may contain anything, depending on the definition 
of the field.
      * <ul>
@@ -396,7 +396,7 @@ enum Type {
     /**
      * The size of this type, in number of bytes.
      */
-    final int size;
+    public final int size;
 
     /**
      * Whether this type is an unsigned integer.
@@ -430,7 +430,7 @@ enum Type {
      * @param  code  the GeoTIFF numerical code.
      * @return the enumeration value that represent the given type, or {@code 
null} if unknown.
      */
-    static Type valueOf(final int code) {
+    public static Type valueOf(final int code) {
         return (code >= 0 && code < FROM_CODES.length) ? FROM_CODES[code] : 
null;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/XMLMetadata.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
similarity index 92%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/XMLMetadata.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
index 6b8910c98a..06464ba596 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/XMLMetadata.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/XMLMetadata.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import java.util.Locale;
 import java.util.Iterator;
@@ -26,6 +26,7 @@ import java.io.IOException;
 import java.io.StringReader;
 import java.io.ByteArrayInputStream;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import javax.xml.stream.XMLEventReader;
@@ -38,9 +39,11 @@ import javax.xml.stream.events.StartElement;
 import javax.xml.transform.stax.StAXSource;
 import javax.xml.namespace.QName;
 import jakarta.xml.bind.JAXBException;
+import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.util.internal.StandardDateFormat;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.geotiff.base.Tags;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TableColumn;
@@ -67,7 +70,7 @@ import static 
org.apache.sis.metadata.internal.TemporalUtilities.toDate;
  *
  * @see <a href="https://www.dgiwg.org/dgiwg-standards";>DGIWG Standards</a>
  */
-final class XMLMetadata implements Filter {
+public final class XMLMetadata implements Filter {
     /**
      * The {@value} string, used in GDAL metadata.
      */
@@ -126,17 +129,22 @@ final class XMLMetadata implements Filter {
     /**
      * Creates new metadata which will decode the given vector of bytes.
      *
-     * @param  reader  the TIFF reader.
-     * @param  type    type of the metadata tag to read.
-     * @param  count   number of bytes or characters in the value to read.
-     * @param  tag     the tag where the metadata was stored.
+     * @param  input      the input channel from which to read the tag.
+     * @param  encoding   the encoding of characters (usually US ASCII).
+     * @param  listeners  where to report warnings.
+     * @param  type       type of the metadata tag to read.
+     * @param  count      number of bytes or characters in the value to read.
+     * @param  tag        the tag where the metadata was stored.
+     * @throws IOException if an error occurred while reading the TIFF tag 
content.
      */
-    XMLMetadata(final Reader reader, final Type type, final long count, final 
short tag) throws IOException {
+    public XMLMetadata(final ChannelDataInput input, final Charset encoding, 
final StoreListeners listeners,
+                       final Type type, final long count, final short tag) 
throws IOException
+    {
+        this.listeners = listeners;
         isGDAL = (tag == Tags.GDAL_METADATA);
-        listeners = reader.store.listeners();
         switch (type) {
             case ASCII: {
-                final String[] cs = type.readAsStrings(reader.input, count, 
reader.store.encoding);
+                final String[] cs = type.readAsStrings(input, count, encoding);
                 switch (cs.length) {
                     case 0:  break;
                     case 1:  string = cs[0]; break;      // Usual case.
@@ -150,7 +158,7 @@ final class XMLMetadata implements Filter {
                  * NoSuchElementException, ClassCastException and 
UnsupportedOperationException
                  * should never happen here because we verified that the 
vector type is byte.
                  */
-                bytes = ((ByteBuffer) type.readAsVector(reader.input, 
count).buffer().get()).array();
+                bytes = ((ByteBuffer) type.readAsVector(input, 
count).buffer().get()).array();
                 break;
             }
         }
@@ -189,6 +197,7 @@ final class XMLMetadata implements Filter {
     /**
      * Returns the XML document as a character string, or {@code null} if the 
document could not be read.
      */
+    @Override
     public String toString() {
         if (string == null) {
             if (bytes == null) {
@@ -218,7 +227,7 @@ final class XMLMetadata implements Filter {
      * The root node contains the XML document as a {@linkplain 
#getUserObject() user object}.
      * It allows JavaFX application to support the "copy to clipboard" 
operation.
      */
-    static final class Root extends DefaultTreeTable.Node {
+    public static final class Root extends DefaultTreeTable.Node {
         /**
          * For cross-version compatibility.
          */
@@ -226,13 +235,15 @@ final class XMLMetadata implements Filter {
 
         /**
          * Column for the name associated to the element.
+         * Should be same as {@code NativeMetadata.NAME}.
          */
-        private static final TableColumn<CharSequence> NAME = 
NativeMetadata.NAME;
+        private static final TableColumn<CharSequence> NAME = TableColumn.NAME;
 
         /**
          * Column for the value associated to the element.
+         * Should be same as {@code NativeMetadata.VALUE}.
          */
-        private static final TableColumn<Object> VALUE = NativeMetadata.VALUE;
+        private static final TableColumn<Object> VALUE = TableColumn.VALUE;
 
         /**
          * A string representation of the XML document.
@@ -264,7 +275,7 @@ final class XMLMetadata implements Filter {
          * @param  name    name to assign to this root node.
          * @return {@code true} on success, or {@code false} if the XML 
document could not be decoded.
          */
-        Root(final XMLMetadata source, final DefaultTreeTable.Node parent, 
final String name) {
+        public Root(final XMLMetadata source, final DefaultTreeTable.Node 
parent, final String name) {
             super(parent);
             xml = source.toString();
             source.currentElement = name;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/package-info.java
similarity index 81%
copy from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
copy to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/package-info.java
index 156321c438..32ae036bf1 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/reader/package-info.java
@@ -16,13 +16,15 @@
  */
 
 /**
- * Utility classes for the implementation of GeoTIFF reader and writer.
- *
- * <STRONG>Do not use!</STRONG>
+ * Helper classes for the GeoTIFF reader.
+ * This package does not contain all reader code.
+ * The main reader class is in the parent package.
  *
  * This package is for internal use by SIS only. Classes in this package
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see org.apache.sis.storage.geotiff.Reader
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.reader;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysWriter.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
similarity index 93%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysWriter.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
index 38f00f0067..b4f8e3c212 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoKeysWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/GeoEncoder.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.writer;
 
 import java.util.List;
 import java.util.EnumMap;
@@ -61,7 +61,11 @@ import org.apache.sis.referencing.util.WKTKeywords;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
 import org.apache.sis.storage.base.MetadataFetcher;
-import org.apache.sis.storage.geotiff.internal.Resources;
+import org.apache.sis.storage.geotiff.base.UnitKey;
+import org.apache.sis.storage.geotiff.base.GeoKeys;
+import org.apache.sis.storage.geotiff.base.GeoCodes;
+import org.apache.sis.storage.geotiff.base.Resources;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.metadata.iso.citation.Citations;
 
 
@@ -73,7 +77,7 @@ import org.apache.sis.metadata.iso.citation.Citations;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class GeoKeysWriter {
+public final class GeoEncoder {
     /**
      * Size of the model transformation matrix, in number of rows and columns.
      * This size is fixed by the GeoTIFF specification.
@@ -81,10 +85,9 @@ final class GeoKeysWriter {
     private static final int MATRIX_SIZE = 4;
 
     /**
-     * The store for which we are writing GeoTIFF keys.
-     * Used for logging warnings.
+     * The listeners where to report warnings.
      */
-    private final GeoTiffStore store;
+    private final StoreListeners listeners;
 
     /**
      * Overall configuration of the GeoTIFF file, or {@code null} if none.
@@ -128,7 +131,7 @@ final class GeoKeysWriter {
 
     /**
      * The key directory, including the header.
-     * Each entry is a record of {@value GeoKeysLoader#ENTRY_LENGTH} values.
+     * Each entry is a record of {@value GeoCodes#ENTRY_LENGTH} values.
      * The first record is a header of the same length.
      *
      * @see #keyCount
@@ -156,7 +159,7 @@ final class GeoKeysWriter {
 
     /**
      * Parameters to encode as ASCII character strings.
-     * Strings are separated by the {@value GeoKeysLoader#SEPARATOR} character.
+     * Strings are separated by the {@value GeoCodes#STRING_SEPARATOR} 
character.
      *
      * @see #asciiParams()
      */
@@ -185,14 +188,14 @@ final class GeoKeysWriter {
      * Prepares information for writing GeoTIFF tags for the given grid 
geometry.
      * Caller shall invoke {@link #write(GridGeometry, MetadataFetcher)} 
exactly once after construction.
      *
-     * @param  store     the store for which to write GeoTIFF keys.
+     * @param  listeners  the listeners where to report warnings.
      */
-    GeoKeysWriter(final GeoTiffStore store) {
-        this.store   = store;
+    public GeoEncoder(final StoreListeners listeners) {
+        this.listeners = listeners;
         units        = new EnumMap<>(UnitKey.class);
         asciiParams  = new StringBuilder(100);
-        doubleParams = new double[GeoKeys.NUM_DOUBLES];
-        keyDirectory = new short[(GeoKeys.NUM_KEYS + 1) * 
GeoKeysLoader.ENTRY_LENGTH];
+        doubleParams = new double[GeoCodes.NUM_DOUBLE_GEOKEYS];
+        keyDirectory = new short[(GeoCodes.NUM_GEOKEYS + 1) * 
GeoCodes.ENTRY_LENGTH];
         keyDirectory[0] = 1;            // Directory version.
         keyDirectory[1] = 1;            // Revision major number. We implement 
GeoTIFF 1.1.
         keyDirectory[2] = 1;            // Revision minor number. We implement 
GeoTIFF 1.1.
@@ -210,7 +213,7 @@ final class GeoKeysWriter {
      * @throws IncommensurableException if a measure uses an unexpected unit 
of measurement.
      * @throws IncompleteGridGeometryException if the grid geometry is 
incomplete.
      */
-    final void write(final GridGeometry grid, final MetadataFetcher<?> 
metadata)
+    public void write(final GridGeometry grid, final MetadataFetcher<?> 
metadata)
                   throws FactoryException, IncommensurableException
     {
         citation  = CollectionsExt.first(metadata.transformationDimension);
@@ -422,7 +425,7 @@ final class GeoKeysWriter {
             if (type != null) {
                 final Unit<?> previous = units.putIfAbsent(type, unit);
                 if (previous != null && !previous.equals(unit)) {
-                    
warning(store.errors().getString(Errors.Keys.HeterogynousUnitsIn_1, name(cs)), 
null);
+                    
warning(errors().getString(Errors.Keys.HeterogynousUnitsIn_1, name(cs)), null);
                 }
             } else {
                 cannotEncode(2, unit.toString(), null);
@@ -465,7 +468,7 @@ final class GeoKeysWriter {
         }
         writeString(key, name);
         citationMainKey = type;
-        citationLengthIndex = keyCount * GeoKeysLoader.ENTRY_LENGTH + 2;       
 // Length is the field #2.
+        citationLengthIndex = keyCount * GeoCodes.ENTRY_LENGTH + 2;        // 
Length is the field #2.
     }
 
     /**
@@ -489,7 +492,7 @@ final class GeoKeysWriter {
                 length += value.length();
                 citationMainKey = null;
             }
-            final String value = GeoKeysLoader.SEPARATOR + type + '=' + name;
+            final String value = GeoCodes.STRING_SEPARATOR + type + '=' + name;
             asciiParams.insert(offset + length, value);
             keyDirectory[i] = toShort(length += value.length());
             /*
@@ -499,10 +502,10 @@ final class GeoKeysWriter {
              * after citation, but we keep it in case a future GeoTIFF version 
adds more ASCII entries.
              */
             final int shift = length - start;
-            final int limit = keyCount * GeoKeysLoader.ENTRY_LENGTH;    // 
Inclusive.
+            final int limit = keyCount * GeoCodes.ENTRY_LENGTH;         // 
Inclusive.
             i++;                                                        // 
Offset is the field after length.
             while (i < limit) {
-                i += GeoKeysLoader.ENTRY_LENGTH;                        // 
Really after (i < limit) test.
+                i += GeoCodes.ENTRY_LENGTH;                             // 
Really after (i < limit) test.
                 if (keyDirectory[i-2] == (short) TAG_GEO_ASCII_PARAMS) {
                     offset = Short.toUnsignedInt(keyDirectory[i]);
                     keyDirectory[i] = toShort(offset + shift);
@@ -527,7 +530,7 @@ final class GeoKeysWriter {
         if (id != null) try {
             return Short.parseShort(id.getCode());
         } catch (NumberFormatException e) {
-            warning(store.errors().getString(Errors.Keys.CanNotParse_1, 
IdentifiedObjects.toString(id)), e);
+            warning(errors().getString(Errors.Keys.CanNotParse_1, 
IdentifiedObjects.toString(id)), e);
         }
         return GeoCodes.userDefined;
     }
@@ -586,7 +589,7 @@ final class GeoKeysWriter {
      * @param  value  the value to store.
      */
     private void writeShort(final short key, final short value) {
-        int i = ++keyCount * GeoKeysLoader.ENTRY_LENGTH;
+        int i = ++keyCount * GeoCodes.ENTRY_LENGTH;
         keyDirectory[i++] = key;            // Key identifier.
         keyDirectory[++i] = 1;              // Number of values in this key.
         keyDirectory[++i] = value;          // Value offset. In this 
particular case, contains directly the value.
@@ -599,7 +602,7 @@ final class GeoKeysWriter {
      * @param  value  the value to store.
      */
     private void writeDouble(final short key, final double value) {
-        int i = ++keyCount * GeoKeysLoader.ENTRY_LENGTH;
+        int i = ++keyCount * GeoCodes.ENTRY_LENGTH;
         keyDirectory[i++] = key;                                // Key 
identifier.
         keyDirectory[i++] = (short) TAG_GEO_DOUBLE_PARAMS;      // TIFF tag 
location.
         keyDirectory[i++] = 1;                                  // Number of 
values in this key.
@@ -614,12 +617,12 @@ final class GeoKeysWriter {
      * @param  value  the value to store.
      */
     private void writeString(final short key, final String value) {
-        int i = ++keyCount * GeoKeysLoader.ENTRY_LENGTH;
+        int i = ++keyCount * GeoCodes.ENTRY_LENGTH;
         keyDirectory[i++] = key;                                // Key 
identifier.
         keyDirectory[i++] = (short) TAG_GEO_ASCII_PARAMS;       // TIFF tag 
location.
         keyDirectory[i++] = toShort(value.length());            // Number of 
values in this key.
         keyDirectory[i  ] = toShort(asciiParams.length());      // Offset of 
the first character.
-        asciiParams.append(value).append(GeoKeysLoader.SEPARATOR);
+        asciiParams.append(value).append(GeoCodes.STRING_SEPARATOR);
     }
 
     /**
@@ -641,16 +644,16 @@ final class GeoKeysWriter {
     /**
      * {@return the values to write in the "GeoTIFF keys directory" tag}.
      */
-    final short[] keyDirectory() {
+    public short[] keyDirectory() {
         if (keyCount == 0) return null;
-        keyDirectory[GeoKeysLoader.ENTRY_LENGTH - 1] = (short) keyCount;
-        return ArraysExt.resize(keyDirectory, (keyCount + 1) * 
GeoKeysLoader.ENTRY_LENGTH);
+        keyDirectory[GeoCodes.ENTRY_LENGTH - 1] = (short) keyCount;
+        return ArraysExt.resize(keyDirectory, (keyCount + 1) * 
GeoCodes.ENTRY_LENGTH);
     }
 
     /**
      * {@return the values to write in the "GeoTIFF double-precision 
parameters" tag}.
      */
-    final double[] doubleParams() {
+    public double[] doubleParams() {
         if (doubleCount == 0) return null;
         return ArraysExt.resize(doubleParams, doubleCount);
     }
@@ -658,7 +661,7 @@ final class GeoKeysWriter {
     /**
      * {@return the values to write in the "GeoTIFF ASCII strings" tag}.
      */
-    final List<String> asciiParams() {
+    public List<String> asciiParams() {
         if (asciiParams.length() == 0) return null;     // TODO: replace by 
isEmpty() with JDK15.
         return List.of(asciiParams.toString());
     }
@@ -668,7 +671,7 @@ final class GeoKeysWriter {
      * Array length is fixed to 16 elements, for a 4×4 matrix in row-major 
order.
      * Axis order is fixed to (longitude, latitude, height).
      */
-    final double[] modelTransformation() {
+    public double[] modelTransformation() {
         if (gridToCRS == null) {
             return null;
         }
@@ -728,14 +731,21 @@ final class GeoKeysWriter {
      * @return the object name.
      */
     private String name(final IdentifiedObject object) {
-        return IdentifiedObjects.getDisplayName(object, store.getLocale());
+        return IdentifiedObjects.getDisplayName(object, listeners.getLocale());
+    }
+
+    /**
+     * {@return the resources for error messages in the current locale}.
+     */
+    private Errors errors() {
+        return Errors.getResources(listeners.getLocale());
     }
 
     /**
      * {@return the resources in the current locale}.
      */
     private Resources resources() {
-        return Resources.forLocale(store.getLocale());
+        return Resources.forLocale(listeners.getLocale());
     }
 
     /**
@@ -775,7 +785,7 @@ final class GeoKeysWriter {
      * @param  cause    the reason why a warning occurred, or {@code null} if 
none.
      */
     private void warning(final String message, final Exception cause) {
-        store.listeners().warning(message, cause);
+        listeners.warning(message, cause);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReformattedImage.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
similarity index 88%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReformattedImage.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
index 9bb0cf275f..a7008d005e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ReformattedImage.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/ReformattedImage.java
@@ -14,8 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.writer;
 
+import java.util.function.Supplier;
 import java.awt.color.ColorSpace;
 import java.awt.image.ColorModel;
 import java.awt.image.IndexColorModel;
@@ -25,6 +26,7 @@ import static javax.imageio.plugins.tiff.BaselineTIFFTagSet.*;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.image.PlanarImage;
+import org.apache.sis.image.ImageProcessor;
 import org.apache.sis.coverage.grid.j2d.ImageUtilities;
 import org.apache.sis.storage.IncompatibleResourceException;
 
@@ -37,11 +39,11 @@ import org.apache.sis.storage.IncompatibleResourceException;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class ReformattedImage {
+public final class ReformattedImage {
     /**
      * The main image with visible bands.
      */
-    final RenderedImage visibleBands;
+    public final RenderedImage visibleBands;
 
     /*
      * TODO: alpha and extra bands not yet stored.
@@ -53,10 +55,10 @@ final class ReformattedImage {
      * The visible image will have at most 3 bands and should have no alpha 
channel.
      * If no change is needed, then the given image is used unchanged.
      *
-     * @param  writer  the writer for which to separate in image.
-     * @param  image   the image to separate into visible, alpha and extra 
bands.
+     * @param  image      the image to separate into visible, alpha and extra 
bands.
+     * @param  processor  supplier of the image processor to use if the image 
must be transformed.
      */
-    ReformattedImage(final Writer writer, final RenderedImage image) {
+    public ReformattedImage(final RenderedImage image, final 
Supplier<ImageProcessor> processor) {
         final int numBands = ImageUtilities.getNumBands(image);
 select: if (numBands > 1) {
             final int[] bands;
@@ -73,7 +75,7 @@ select: if (numBands > 1) {
                 }
                 bands = ArraysExt.range(0, max);
             }
-            visibleBands = writer.processor().selectBands(image, bands);
+            visibleBands = processor.get().selectBands(image, bands);
             return;
         }
         visibleBands = image;
@@ -87,7 +89,7 @@ select: if (numBands > 1) {
      * @return statistics in an array of length 2, with minimums first then 
maximums.
      *         Array elements may be {@code null} if there is no statistics.
      */
-    final double[][] statistics(final int numBands) {
+    public double[][] statistics(final int numBands) {
         final Object property = 
visibleBands.getProperty(PlanarImage.STATISTICS_KEY);
 found:  if (property instanceof Statistics[]) {
             final var stats = (Statistics[]) property;
@@ -109,7 +111,7 @@ found:  if (property instanceof Statistics[]) {
      *
      * @return One of {@code SAMPLE_FORMAT_*} constants.
      */
-    final int getSampleFormat() {
+    public int getSampleFormat() {
         final SampleModel sm = visibleBands.getSampleModel();
         if (ImageUtilities.isUnsignedType(sm)) return 
SAMPLE_FORMAT_UNSIGNED_INTEGER;
         if (ImageUtilities.isIntegerType(sm))  return 
SAMPLE_FORMAT_SIGNED_INTEGER;
@@ -122,7 +124,7 @@ found:  if (property instanceof Statistics[]) {
      * @return One of {@code PHOTOMETRIC_INTERPRETATION_*} constants.
      * @throws IncompatibleResourceException if the color model is not 
supported.
      */
-    final int getColorInterpretation() throws IncompatibleResourceException {
+    public int getColorInterpretation() throws IncompatibleResourceException {
         final ColorModel  cm = visibleBands.getColorModel();
         if (cm instanceof IndexColorModel) {
             final var   icm   = (IndexColorModel) cm;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TagValueWriter.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TagValue.java
similarity index 56%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TagValueWriter.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TagValue.java
index f46e6fffb1..07136c568c 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TagValueWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TagValue.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.writer;
 
 import java.io.IOException;
 import org.apache.sis.io.stream.UpdatableWrite;
@@ -28,26 +28,62 @@ import org.apache.sis.io.stream.ChannelDataOutput;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-abstract class TagValueWriter {
+public abstract class TagValue {
     /**
      * A handler for writing the position of tag values when this position 
will become known.
-     * This is initialized by {@link Writer#writeLargeTag(short, short, long, 
TagValueWriter)}.
+     * This is initialized by {@link Writer#writeLargeTag(short, short, long, 
TagValue)}.
      */
-    UpdatableWrite<?> offset;
+    private UpdatableWrite<?> offset;
 
     /**
      * Creates a new container for the values of a tag.
      */
-    TagValueWriter() {
+    public TagValue() {
     }
 
     /**
      * Writes the values of the tag at the current position of the given 
output stream.
      *
-     * @param  output  the {@link Writer#output} value, provided for 
convenience.
+     * @param  output  the stream where to write tag values.
+     * @throws IOException if an error occurred while writing the tag values.
+     */
+    protected abstract void write(ChannelDataOutput output) throws IOException;
+
+    /**
+     * Remembers the position where the tag will be written.
+     *
+     * @param  offset  a handler for writing the position.
+     */
+    public final void mark(final UpdatableWrite<?> offset) {
+        this.offset = offset;
+    }
+
+    /**
+     * Remembers the current stream position, then writes the values of the 
tag.
+     *
+     * @param  output  the stream where to write tag values.
      * @throws IOException if an error occurred while writing the tag values.
      */
-    abstract void write(ChannelDataOutput output) throws IOException;
+    public final void markAndWrite(final ChannelDataOutput output) throws 
IOException {
+        offset = UpdatableWrite.of(output);        // Record only the position.
+        write(output);
+    }
+
+    /**
+     * Writes at the current position and update the pointer to that position.
+     * If the pointer can be updated immediately, this method returns {@code 
null}.
+     * Otherwise this method returns a handler for updating the pointer later.
+     *
+     * @param  output  the stream where to write tag values.
+     * @return a handler for updating the position if it couldn't be written 
now.
+     * @throws IOException if an error occurred while writing the tag values.
+     */
+    public final UpdatableWrite<?> writeHere(final ChannelDataOutput output) 
throws IOException {
+        offset.setAsLong(output.getStreamPosition());
+        boolean done = offset.tryUpdateBuffer(output);
+        write(output);
+        return done ? null : offset;
+    }
 
     /**
      * Writes again the values at the same offset than previously.
@@ -56,7 +92,7 @@ abstract class TagValueWriter {
      * @param  output  the stream where to write tag values.
      * @throws IOException if an error occurred while writing the tag values.
      *
-     * @see TileMatrixWriter#isLengthChanged()
+     * @see TileMatrix#isLengthChanged()
      */
     final void rewrite(final ChannelDataOutput output) throws IOException {
         /*
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TileMatrixWriter.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
similarity index 93%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TileMatrixWriter.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
index 9843019ba6..ed080fb0ef 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/TileMatrixWriter.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/TileMatrix.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.writer;
 
 import java.util.Arrays;
 import java.io.IOException;
@@ -44,11 +44,11 @@ import org.apache.sis.io.stream.HyperRectangleWriter;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class TileMatrixWriter {
+public final class TileMatrix {
     /**
      * Offset in {@link ChannelDataOutput} where the IFD starts.
      */
-    final long offsetIFD;
+    public final long offsetIFD;
 
     /**
      * The images to write.
@@ -73,7 +73,7 @@ final class TileMatrixWriter {
     /**
      * Size of each tile.
      */
-    final int tileWidth, tileHeight;
+    public final int tileWidth, tileHeight;
 
     /**
      * Uncompressed size of tiles in number of bytes, as an unsigned integer.
@@ -83,17 +83,17 @@ final class TileMatrixWriter {
     /**
      * Compressed size of each tile in number of bytes, as unsigned integers.
      */
-    final int[] lengths;
+    public final int[] lengths;
 
     /**
      * Offsets to each tiles. Not necessarily in increasing order (it depends 
on tile order).
      */
-    final long[] offsets;
+    public final long[] offsets;
 
     /**
      * Tags where are stored offsets and lengths.
      */
-    TagValueWriter offsetsTag, lengthsTag;
+    public TagValue offsetsTag, lengthsTag;
 
     /**
      * Creates a new set of information about tiles to write.
@@ -104,8 +104,8 @@ final class TileMatrixWriter {
      * @param isPlanar       whether the planar configuration is to store 
bands in separated planes.
      * @param offsetIFD      offset in {@link ChannelDataOutput} where the IFD 
starts.
      */
-    TileMatrixWriter(final RenderedImage image, final int numPlanes, final 
int[] bitsPerSample,
-                     final long offsetIFD)
+    public TileMatrix(final RenderedImage image, final int numPlanes, final 
int[] bitsPerSample,
+                      final long offsetIFD)
     {
         final int pixelSize, numArrays;
         this.offsetIFD = offsetIFD;
@@ -131,7 +131,7 @@ final class TileMatrixWriter {
      *
      * @throws IOException if an error occurred while writing to the output 
stream.
      */
-    final void writeOffsetsAndLengths(final ChannelDataOutput output) throws 
IOException {
+    public void writeOffsetsAndLengths(final ChannelDataOutput output) throws 
IOException {
         offsetsTag.rewrite(output);
         for (int value : lengths) {
             if (value != tileSize) {
@@ -149,7 +149,7 @@ final class TileMatrixWriter {
      * @param  output  where to write the tiles data.
      * @throws IOException if an error occurred while writing to the given 
output.
      */
-    final void writeRasters(final ChannelDataOutput output) throws IOException 
{
+    public void writeRasters(final ChannelDataOutput output) throws 
IOException {
         SampleModel sm = null;
         int[] bankIndices = null;
         HyperRectangleWriter rect = null;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/package-info.java
similarity index 81%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/package-info.java
index 156321c438..d0625e6a05 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/internal/package-info.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/writer/package-info.java
@@ -16,13 +16,15 @@
  */
 
 /**
- * Utility classes for the implementation of GeoTIFF reader and writer.
- *
- * <STRONG>Do not use!</STRONG>
+ * Helper classes for the GeoTIFF writer.
+ * This package does not contain all writer code.
+ * The main writer class is in the parent package.
  *
  * This package is for internal use by SIS only. Classes in this package
  * may change in incompatible ways in any future version without notice.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see org.apache.sis.storage.geotiff.Writer
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.writer;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
index 8481ec8d0e..a6e235f514 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java
@@ -37,6 +37,7 @@ import org.apache.sis.io.stream.ByteArrayChannel;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.geotiff.base.Tags;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridOrientation;
@@ -182,7 +183,7 @@ public final class WriterTest extends TestCase {
     public void testUntiledGrayScale() throws IOException, DataStoreException {
         initialize(DataType.BYTE, ByteOrder.BIG_ENDIAN, false, 1, 1, 1);
         writeImage();
-        verifyHeader(false, GeoTIFF.BIG_ENDIAN);
+        verifyHeader(false, IOBase.BIG_ENDIAN);
         verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, 
PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO,
                                  new short[] {Byte.SIZE});
         verifySampleValues(1);
@@ -199,7 +200,7 @@ public final class WriterTest extends TestCase {
     public void testUntiledBigTIFF() throws IOException, DataStoreException {
         initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 1, 1, 
GeoTiffOption.BIG_TIFF);
         writeImage();
-        verifyHeader(true, GeoTIFF.LITTLE_ENDIAN);
+        verifyHeader(true, IOBase.LITTLE_ENDIAN);
         verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, 
PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO,
                                  new short[] {Byte.SIZE});
         verifySampleValues(1);
@@ -217,7 +218,7 @@ public final class WriterTest extends TestCase {
     public void testTiledGrayScale() throws IOException, DataStoreException {
         initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 3, 4);
         writeImage();
-        verifyHeader(false, GeoTIFF.LITTLE_ENDIAN);
+        verifyHeader(false, IOBase.LITTLE_ENDIAN);
         verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, 
PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO,
                                  new short[] {Byte.SIZE});
         verifySampleValues(1);
@@ -235,7 +236,7 @@ public final class WriterTest extends TestCase {
         initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 3, 1, 1);
         
image.setColorModel(ColorModelFactory.createRGB(image.getSampleModel()));
         writeImage();
-        verifyHeader(false, GeoTIFF.LITTLE_ENDIAN);
+        verifyHeader(false, IOBase.LITTLE_ENDIAN);
         verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS, 
PHOTOMETRIC_INTERPRETATION_RGB,
                                  new short[] {Byte.SIZE, Byte.SIZE, 
Byte.SIZE});
         verifySampleValues(3);
@@ -253,7 +254,7 @@ public final class WriterTest extends TestCase {
         initialize(DataType.BYTE, ByteOrder.LITTLE_ENDIAN, false, 1, 1, 1);
         createGridGeometry();
         writeImage();
-        verifyHeader(false, GeoTIFF.LITTLE_ENDIAN);
+        verifyHeader(false, IOBase.LITTLE_ENDIAN);
         verifyImageFileDirectory(Writer.MINIMAL_NUMBER_OF_TAGS + 3,            
         // GeoTIFF adds 3 tags.
                 PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO, new short[] 
{Byte.SIZE});
         verifySampleValues(1);
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/internal/CompressionTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/CompressionTest.java
similarity index 96%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/internal/CompressionTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/CompressionTest.java
index 59b8d1837d..9191f906e0 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/internal/CompressionTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/CompressionTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff.internal;
+package org.apache.sis.storage.geotiff.base;
 
 // Test dependencies
 import org.junit.Test;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoCodesTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoCodesTest.java
similarity index 98%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoCodesTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoCodesTest.java
index dc7d67b151..a89cc35885 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoCodesTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoCodesTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterDescriptorGroup;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoIdentifiers.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoIdentifiers.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoIdentifiers.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoIdentifiers.java
index d357f1cd05..a009f2d86b 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoIdentifiers.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoIdentifiers.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import java.lang.reflect.Field;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoKeysTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoKeysTest.java
similarity index 97%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoKeysTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoKeysTest.java
index ef149eede8..955a15831c 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoKeysTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/GeoKeysTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 import java.util.Set;
 import java.lang.reflect.Field;
@@ -129,11 +129,11 @@ public final class GeoKeysTest extends TestCase {
     }
 
     /**
-     * Verifies the value of {@link GeoKeys#NUM_KEYS}.
+     * Verifies the value of {@link GeoCodes#NUM_GEOKEYS}.
      */
     @Test
     public void verifyNumKeys() {
         final Field[] fields = GeoKeys.class.getFields();       // Include 
only public fields.
-        assertEquals(fields.length, GeoKeys.NUM_KEYS);
+        assertEquals(fields.length, GeoCodes.NUM_GEOKEYS);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TagsTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/TagsTest.java
similarity index 97%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TagsTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/TagsTest.java
index b89acd17ef..2f3ba3797e 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TagsTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/base/TagsTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.base;
 
 // Test dependencies
 import org.junit.Test;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/CRSBuilderTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/CRSBuilderTest.java
similarity index 98%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/CRSBuilderTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/CRSBuilderTest.java
index 6f431df2e7..a4789b2b6a 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/CRSBuilderTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/CRSBuilderTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 // Test dependencies
 import org.junit.Test;
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TypeTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/TypeTest.java
similarity index 98%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TypeTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/TypeTest.java
index 695e052905..2d98989c87 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/TypeTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/TypeTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import org.apache.sis.io.stream.ChannelDataInput;
 
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/XMLMetadataTest.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/XMLMetadataTest.java
similarity index 99%
rename from 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/XMLMetadataTest.java
rename to 
endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/XMLMetadataTest.java
index 8be6dcd852..e3c2f05aab 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/XMLMetadataTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/reader/XMLMetadataTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.geotiff;
+package org.apache.sis.storage.geotiff.reader;
 
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.storage.base.MetadataBuilder;
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
index 0784362639..d02ccefe7c 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
@@ -219,7 +219,7 @@ public abstract class MetadataFetcher<T> {
     protected boolean accept(final CitationDate info) {
         if (creationDate == null) {
             if (DateType.CREATION.equals(info.getDateType())) {
-                creationDate = List.of(parseDate(info.getDate()));
+                creationDate = List.of(convertDate(info.getDate()));
             } else {
                 return false;       // Search another date.
             }
@@ -391,5 +391,5 @@ public abstract class MetadataFetcher<T> {
      * @param  date  the date to convert.
      * @return subclass-dependent object representing the given date.
      */
-    protected abstract T parseDate(final Date date);
+    protected abstract T convertDate(final Date date);
 }


Reply via email to