/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.image.io;
import java.io.*; // Many imports, including some for javadoc only.
import java.nio.charset.Charset;
import java.nio.channels.WritableByteChannel;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.Locale;
import java.text.NumberFormat;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.awt.image.DataBuffer;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.iterator.RectIter;
/**
* Base class for image writers that expect a {@link BufferedWriter} output.
* "<cite>Text images</cite>" are usually ASCII files where pixels values are
* actually the geophysical values. This base class provides the following conveniences:
* <p>
* <ul>
* <li>Get a {@link BufferedWriter} from the output types, which may be a any type documented
* in the {@linkplain StreamImageWriter super-class} plus {@link Writer}.</li>
* <li>Offer a {@link #createNumberFormat createNumberFormat(...)} method which set the
* {@linkplain NumberFormat#getMaximumFractionDigits() number of fraction digits} to
* a value determined from the sample values present in the image.</li>
* <li>Get the character encoding and the locale (for formating numbers) from the fields declared
* in the {@linkplain Spi Service Provider}. Alternatively, subclasses can also get more
* control by overriding the {@link #getCharset(ImageWriteParam)} method.</li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.08
*
* @see TextImageReader
*
* @since 3.08 (derived from 1.2)
* @module
*/
public abstract class TextImageWriter extends StreamImageWriter {
/**
* Maximum number of digits to be allowed by {@link #createNumberFormat}.
*/
private static final int MAXIMUM_DIGITS = 12;
/**
* Do not force the formatting of fraction digits if the sample values are equal or
* greater than 1E+6.
*/
private static final double NODIGITS_THRESHOLD = 1E+6;
/**
* Number of digits to check after the last one, as 10<sup>-n</sup>. The default value is
* 1E-1. This means that if the digit immediately after the last one is 0, we will consider
* that we have reached the intended precision.
* <p>
* For adjusting the number of digits to check after the last one, just put this number
* as a negative power in place of the "-1" above.
*/
private static final double DELTA_THRESHOLD = 1E-1;
/**
* The maximum value found during the last call to {@link #createNumberFormat}.
*/
private double maximum;
/**
* {@link #output} as a buffered writer, or {@code null} if none.
*
* @see #getWriter
*/
private BufferedWriter writer;
/**
* Constructs a {@code TextImageWriter}.
*
* @param provider The {@link ImageWriterSpi} that is constructing this object, or {@code null}.
*/
protected TextImageWriter(final Spi provider) {
super(provider);
}
/**
* Returns the locale to use for encoding values, or {@code null} for the
* {@linkplain Locale#getDefault default}. The default implementation returns the
* {@linkplain Spi#locale locale} specified to the {@code Spi} object given to this
* {@code TextImageWriter} constructor. Subclasses can override this method if they
* want to specify the data locale in some other way.
* <p>
* <b>Note:</b> This locale should not be confused with {@link #getLocale}.
*
* @param parameters The write parameters, or {@code null} for the defaults.
* @return The locale to use for parsing numbers in the image file.
*
* @see Spi#locale
*/
protected Locale getDataLocale(final ImageWriteParam parameters) {
return (originatingProvider instanceof Spi) ? ((Spi) originatingProvider).locale : null;
}
/**
* Returns the character set to use for encoding the string to the output stream.
* The default implementation returns the {@linkplain Spi#charset character set}
* specified to the {@code Spi} object given to this {@code TextImageWriter} constructor.
* Subclasses can override this method if they want to specify the character encoding in
* some other way.
*
* @param parameters The write parameters, or {@code null} for the defaults.
* @return The character encoding, or {@code null} for the platform default encoding.
* @throws IOException If reading from the output stream failed.
*
* @see Spi#charset
*/
protected Charset getCharset(final ImageWriteParam parameters) throws IOException {
return (originatingProvider instanceof Spi) ? ((Spi) originatingProvider).charset : null;
}
/**
* Returns the line separator to use when writing to the output stream. The default
* implementation returns the {@linkplain Spi#lineSeparator line separator} specified
* to the {@link Spi} object given to this {@code TextImageWriter} constructor. Subclasses
* can override this method if they want to specify the line separator in some other way.
*
* @param parameters The write parameters, or {@code null} for the defaults.
* @return The line separator to use for writing the image.
*
* @see Spi#lineSeparator
*/
protected String getLineSeparator(final ImageWriteParam parameters) {
if (originatingProvider instanceof Spi) {
final String lineSeparator = ((Spi) originatingProvider).lineSeparator;
if (lineSeparator != null) {
return lineSeparator;
}
}
return System.lineSeparator();
}
/**
* Returns the {@linkplain #output output} as a writer. If the output is already a buffered
* writer, then it is returned unchanged. Otherwise the default implementation creates a new
* {@link BufferedWriter} from various output types including {@link File}, {@link URL},
* {@link URLConnection}, {@link Writer}, {@link OutputStream} and {@link ImageOutputStream}.
* <p>
* This method creates the new writer only when first invoked. All subsequent calls will
* returns the same instance. Consequently, the returned writer should never be closed by
* the caller. It may be {@linkplain #close closed} automatically when {@link #setOutput
* setOutput(Object)}, {@link #reset() reset()} or {@link #dispose() dispose()} methods
* are invoked.
*
* @param parameters The write parameters, or {@code null} for the defaults.
* @return {@link #getOutput} usually as a {@link BufferedWriter}.
* @throws IllegalStateException if the {@linkplain #output output} is not set.
* @throws IOException If the output stream can't be created for an other reason.
*
* @see #getOutput
* @see #getOutputStream
*/
protected BufferedWriter getWriter(final ImageWriteParam parameters)
throws IllegalStateException, IOException
{
if (writer == null) {
final Object output = getOutput();
if (output instanceof BufferedWriter) {
writer = (BufferedWriter) output;
closeOnReset = null; // We don't own the underlying writer, so don't close it.
} else if (output instanceof Writer) {
writer = new BufferedWriter((Writer) output);
closeOnReset = null; // We don't own the underlying writer, so don't close it.
} else {
final OutputStream stream = getOutputStream();
final Charset charset = getCharset(parameters);
writer = new BufferedWriter((charset != null) ?
new OutputStreamWriter(stream, charset) : new OutputStreamWriter(stream));
if (closeOnReset == stream) {
closeOnReset = writer;
}
}
}
return writer;
}
/**
* Returns a number format to be used for formatting the sample values in the given image.
*
* @param image The image or raster to be written.
* @param parameters The write parameters, or {@code null} if the whole image will be written.
* @return A number format appropriate for the given image.
*/
protected NumberFormat createNumberFormat(final IIOImage image, final ImageWriteParam parameters) {
final Locale locale = getDataLocale(parameters);
final int type = image.hasRaster() ? image.getRaster().getTransferType() :
image.getRenderedImage().getSampleModel().getDataType();
if (type != DataBuffer.TYPE_FLOAT && type != DataBuffer.TYPE_DOUBLE) {
maximum = (1 << DataBuffer.getDataTypeSize(type)) - 1;
return (locale != null) ? NumberFormat.getIntegerInstance(locale)
: NumberFormat.getIntegerInstance();
}
int digits = 0;
double multiple = 1;
maximum = Double.NEGATIVE_INFINITY;
final RectIter iterator = createRectIter(image, parameters);
if (!iterator.finishedBands()) do {
if (!iterator.finishedLines()) do {
if (!iterator.finishedPixels()) do {
final double value = Math.abs(iterator.getSampleDouble());
if (Double.isInfinite(value)) {
continue;
}
// Following code is NaN tolerant - no need for explicit check.
if (value > maximum) {
maximum = value;
}
while (true) {
double scaled = value * multiple;
if (type == DataBuffer.TYPE_FLOAT) {
scaled = (float) scaled; // Drops the extra digits.
}
// Condition below uses '!' in order to cath NaN values.
if (!(Math.abs(scaled - Math.rint(scaled)) >= DELTA_THRESHOLD)) {
break;
}
if (++digits > MAXIMUM_DIGITS) {
return createNumberFormat(locale);
}
multiple *= 10;
}
} while (!iterator.nextPixelDone());
iterator.startPixels();
} while (!iterator.nextLineDone());
iterator.startLines();
} while (!iterator.nextBandDone());
/*
* 'digits' should now be the exact number of fraction digits to format. However the above
* algorithm do not work if all values are smaller (in absolute value) to DELTA_THRESHOLD,
* in which case 'digits' is still set to 0. In such case it is better to keep the default
* format unchanged, since it should be generic enough.
*/
final NumberFormat format = createNumberFormat(locale);
if (digits != 0 || maximum >= DELTA_THRESHOLD) {
format.setMaximumFractionDigits(digits);
if (maximum < NODIGITS_THRESHOLD) {
format.setMinimumFractionDigits(digits);
}
}
return format;
}
/**
* Returns the number format for the given locale. If the given locale is null,
* then the default locale is used.
*/
private static NumberFormat createNumberFormat(final Locale locale) {
return (locale != null) ? NumberFormat.getNumberInstance(locale) : NumberFormat.getNumberInstance();
}
/**
* Returns the expected position of the fraction part for numbers to be formatted using the
* given format. This method should be invoked after {@link #createNumberFormat}, but the
* given format doesn't need to be the instance returned by the later.
*
* @param format The format to be used for formatting numbers.
* @return The expected position of the fraction part.
*/
protected FieldPosition getExpectedFractionPosition(final NumberFormat format) {
final int minimumDigits = (int) Math.floor(Math.log10(maximum)) + 1;
int width = Math.max(format.getMinimumIntegerDigits(), minimumDigits);
int digits = Math.min(format.getMaximumFractionDigits(), MAXIMUM_DIGITS);
if (format instanceof DecimalFormat) {
final DecimalFormat decimal = (DecimalFormat) format;
if (digits > 0 || decimal.isDecimalSeparatorAlwaysShown()) {
width++;
}
width += Math.max(decimal.getNegativePrefix().length(),
decimal.getPositivePrefix().length());
digits += Math.max(decimal.getNegativeSuffix().length(),
decimal.getPositiveSuffix().length());
}
final FieldPosition position = new FieldPosition(NumberFormat.FRACTION_FIELD);
position.setBeginIndex(width);
position.setEndIndex(width += digits); // NOSONAR (see below)
// 'width' is now the full width. We don't do anything with it at this time,
// but maybe in some future version...
return position;
}
/**
* Closes the writer created by {@link #getWriter getWriter(...)}. This method does nothing
* if the writer is the {@linkplain #output output} instance given by the user rather than
* a writer created by this class from a {@link File} or {@link URL} output.
*
* @throws IOException If an error occurred while closing the writer.
*
* @see #closeOnReset
*/
@Override
protected void close() throws IOException {
writer = null;
super.close();
}
/**
* Service provider interface (SPI) for {@link TextImageWriter}. This SPI provides additional
* fields controlling the character encoding ({@link #charset}), the local to use for formating
* numbers, dates or other objects ({@link #locale}) and the line separator
* ({@link #lineSeparator}).
* <p>
* By default the {@code charset}, {@code locale} and {@code lineSeparator} fields are
* initialized to {@code null}, which stands for the platform-dependent defaults. If a
* subclass wants to fix the encoding and locale to some format-specific values, it shall
* specify those values at construction time as in the example below:
*
* {@preformat java
* public Spi() {
* charset = Charset.forName("ISO-8859-1"); // ISO Latin Alphabet No. 1
* locale = Locale.US;
* }
* }
*
* The table below summarizes the initial values.
* Those values can be modified by subclass constructors.
* <p>
* <table border="1">
* <tr bgcolor="lightblue">
* <th>Field</th>
* <th>Value</th>
* </tr><tr>
* <td> {@link #outputTypes} </td>
* <td> {@link String}, {@link File}, {@link URI}, {@link URL}, {@link URLConnection},
* {@link Writer}, {@link OutputStream}, {@link ImageOutputStream},
* {@link WritableByteChannel} </td>
* </tr><tr>
* <td> {@link #suffixes} </td>
* <td> {@code "txt"}, {@code "TXT"},
* {@code "asc"}, {@code "ASC"},
* {@code "dat"}, {@code "DAT"} </td>
* </tr><tr>
* <td> {@link #charset} </td>
* <td> {@code null} (stands for the
* {@linkplain Charset#defaultCharset() platform default}) </td>
* </tr><tr>
* <td> {@link #locale} </td>
* <td> {@code null} (stands for the
* {@linkplain Locale#getDefault() platform default}) </td>
* </tr><tr>
* <td> {@link #lineSeparator} </td>
* <td> {@code null} (stands for the platform default) </td>
* </tr><tr>
* <td colspan="2" align="center">See
* {@linkplain org.geotoolkit.image.io.SpatialImageWriter.Spi super-class javadoc}
* for remaining fields</td>
* </tr>
* </table>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.07
*
* @see TextImageReader.Spi
*
* @since 3.08 (derived from 2.4)
* @module
*/
protected abstract static class Spi extends StreamImageWriter.Spi {
/**
* List of legal output types for {@link TextImageWriter}.
*/
private static final Class<?>[] OUTPUT_TYPES = new Class<?>[] {
File.class,
URI.class,
URL.class,
URLConnection.class,
Writer.class,
OutputStream.class,
ImageOutputStream.class,
WritableByteChannel.class,
String.class // To be interpreted as file path.
};
/**
* Character encoding, or {@code null} for the default. This field is initially
* {@code null}. A value shall be set by subclasses if the files to be encoded
* use some specific character encoding.
*
* @see TextImageWriter#getCharset(ImageWriteParam)
*/
protected Charset charset;
/**
* The locale for numbers formatting. For example {@link Locale#US} means that
* numbers are expected to use dot as decimal separator. This field is initially
* {@code null}, which means that default locale should be used.
*
* @see TextImageWriter#getDataLocale(ImageWriteParam)
*/
protected Locale locale;
/**
* The line separator to use, or {@code null} for the system default.
*
* @see TextImageWriter#getLineSeparator(ImageWriteParam)
*/
protected String lineSeparator;
/**
* Constructs a quasi-blank {@code TextImageWriter.Spi}. This constructor initializes
* the fields as documented in the <a href="#skip-navbar_top">class javadoc</a>. It is
* up to the subclass to initialize all other instance variables in order to provide
* working versions of all methods.
* <p>
* For efficiency reasons, the above fields are initialized to shared arrays. Subclasses
* can assign new arrays, but should not modify the default array content.
*/
protected Spi() {
outputTypes = OUTPUT_TYPES;
suffixes = TextImageReader.Spi.SUFFIXES;
}
}
}