/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.internal.image.io;
import java.io.*;
import javax.imageio.IIOException;
import java.awt.geom.AffineTransform;
import java.net.URL;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import org.geotoolkit.lang.Static;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.io.ContentFormatException;
/**
* Utility methods related to the additional files that come with some image formats.
* Those additional files have the {@code ".tfw"} or {@code ".prj"} suffixes.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.11
*
* @since 3.00
* @module
*/
public final class SupportFiles extends Static {
/**
* The encoding of TFW files.
*/
private static final String ENCODING = "ISO-8859-1";
/**
* Sequences of TFW suffixes which don't follow the usual rules. The usual rule is to either
* append {@code 'w'} to the suffix, or to keep only the first and the last letter of the
* suffix and append {@code 'w'} to that.
* <p>
* In the list of arrays below, the first element of each array is the suffix for wich a
* special case is needed, and the following elements are the special cases.
*
* @since 3.10
*/
private static final String[][] SPECIAL_CASES = {
new String[] {"jpg", "jpw", "jpegw"}, // No need to declare "jgw" and "jpgw".
new String[] {"jpeg", "jpw", "jpgw"}, // No need to declare "jgw" and "jpegw".
new String[] {"tif", "tiffw", "twfx"},
new String[] {"bmp", "bmw"}
};
/**
* Do not allow instantiation of this class.
*/
private SupportFiles() {
}
/**
* Returns a file with the same path than the given one, except for the extension
* which has been replaced by the given one. If the file already has this extension
* (according a case-insensitive comparison), then it is returned unchanged.
* <p>
* This method returns preferably a file with lower case extension. However if no
* such file exists but a file of the same name with upper case extension exists,
* then the later is returned.
* <p>
* If none of the above was found and the {@code tfw} argument is {@code true}, then
* if a file with the same name and extension than the given {@code file}, with only
* a {@code 'w'} character appended, is found, then that file is returned. Otherwise
* if a file with the {@code tfw} extension exists, that file is returned.
* <p>
* If all the above fail, then the returned file is the one with the given extension.
*
* TODO use NIO glob pattern and DirectoryStream
*
* @param file The image file path.
* @param extension The wanted extension in lower cases and without the dot separator.
* @param isTFW {@code true} if this method is invoked for the TFW file.
* @return the file Path with the given extension.
*/
@SuppressWarnings("fallthrough")
private static Path toSupportFile(final Path file, final String extension, final boolean isTFW) {
final Path parent = file.getParent();
final StringBuilder buffer = new StringBuilder(file.getFileName().toString());
int base = buffer.lastIndexOf(".");
final String currentExtension;
if (base >= 0) {
currentExtension = buffer.substring(++base);
buffer.setLength(base);
} else {
currentExtension = "";
base = buffer.append('.').length();
}
Path fallback = file; // To be used only if no existing file is found.
String[] specialCases = null; // To be used only if the standard cases didn't worked.
int specialCaseIndex = 0;
attmpt: for (int caseNumber=0; ; caseNumber++) {
switch (caseNumber) {
/*
* Try with the preferred extension given in argument. For TFW files, this is
* the first letter, the last letter and 'w'. Example: "pgw" for "png" files.
*/
case 0: {
buffer.append(extension);
break;
}
/*
* Same extension than above, but with upper cases. Exemple: "PGW" for "png"
* files. This is the last attempt made for files that are not TFW files.
*/
case 1: {
buffer.append(extension.toUpperCase());
break;
}
/*
* If we are looking for a TFW file, try the extension of the existing file
* with the 'w' letter appended. Exemple: "pngw" for "png" files.
*/
case 2: {
if (!isTFW) {
break attmpt; // Every cases below this point are for TFW files only.
}
buffer.append(currentExtension).append('w');
break;
}
/*
* Same than above, but in upper cases. Example: "PNGW" for "png" files.
*/
case 3: {
buffer.append(currentExtension.toUpperCase()).append('W');
break;
}
/*
* Get the list of special cases, which will be tested in the next block.
* If no special cases are found, we will skip the next two switch cases.
*/
case 4: {
for (final String[] candidate : SPECIAL_CASES) {
if (currentExtension.equalsIgnoreCase(candidate[0])) {
specialCases = candidate;
break;
}
}
if (specialCases == null) {
caseNumber += 2; // Skip the next 2 switch cases.
continue;
}
caseNumber++;
// fall through
}
/*
* Try the special case in lower cases. Example: "bmw" for "bmp" files.
*/
case 5: {
buffer.append(specialCases[++specialCaseIndex]);
break;
}
/*
* Same than above, but in upper case. Example: "BMW" for "bmp" files. If there
* is more special cases, we will redo this block and the previous one.
*/
case 6: {
buffer.append(specialCases[specialCaseIndex].toUpperCase());
if (specialCaseIndex + 1 != specialCases.length) {
caseNumber -= 2; // Go back 2 switch cases.
}
break;
}
/*
* Check the "tfw" extension, if it was not already done. Note that the
* 'extension' argument is always in lower cases, so it can not be equal
* to "TFW". If those two last attempts didn't worked, we are done.
*/
case 7: {
if (extension.equals("tfw")) {
continue;
}
buffer.append("tfw");
break;
}
case 8: {
buffer.append("TFW");
break;
}
default: {
break attmpt;
}
}
final Path candidate = parent.resolve(buffer.toString());
if (Files.isRegularFile(candidate)) {
return candidate;
}
buffer.setLength(base);
// Retain the first attempt, which will be
// used as a fallback if no file was found.
if (caseNumber == 0) {
fallback = candidate;
}
}
return fallback;
}
/**
* Returns a new file or URL equivalent to the given {@link Path}, {@link String}, {@link File}, {@link URL}
* or {@link URI} argument, with its extension replaced by the given one. The given extension
* shall be all lowercase and without leading dot character.
* <p>
* The {@code "tfw"} extension is handled especially, in that {@code "tfw} will actually be
* used only as a fallback if no file exist with the extension for <cite>World File</cite>.
* <p>
* While not mandatory, it is recommended to invoke {@link org.geotoolkit.nio.IOUtilities#tryToPath(Object)}
* before this method in order to increase the chances to pass a {@link File} argument.
* This allows us to check if the file exists.
*
* @param path The path as a {@link Path}, {@link String}, {@link File}, {@link URL} or {@link URI}.
* @param extension The new extension, in lower cases and without leading dot.
* @return The path with the new extension, or {@code null} if the given path was null.
* @throws IOException If the given object is not recognized, or attempt to replace it
* extension does not result in a valid URL.
*
* @since 3.07
*/
public static Object changeExtension(final Object path, String extension) throws IOException {
if (path != null) {
boolean isTFW = extension.equals("tfw");
if (isTFW) {
extension = toSuffixTFW(path);
}
if (path instanceof Path) {
return toSupportFile((Path) path, extension, isTFW);
}
if (path instanceof File) {
return toSupportFile(((File) path).toPath(), extension, isTFW).toFile();
}
final Object renamed = IOUtilities.changeExtension(path, extension);
if (renamed != null) {
return renamed;
}
throw new IIOException(Errors.format(Errors.Keys.UnknownType_1, path.getClass()));
}
return path;
}
/**
* Returns the TFW suffix for the given file, URL or URI.
* This method returns always the suffix in lower case.
*
* @param file The file for which we want to change the suffix.
* @return The TFW suffix of the given file.
*/
public static String toSuffixTFW(final Object file) {
final String ext = IOUtilities.extension(file);
final int length = ext.length();
if (length >= 2) {
return String.valueOf(new char[] {
Character.toLowerCase(ext.charAt(0)),
Character.toLowerCase(ext.charAt(length - 1)),
'w'
});
}
return "tfw";
}
/**
* Writes the given affine transform as a TFW file.
*
* @param file The filename of the <strong>image</strong>. The suffix will be replaced.
* @param tr The affine transform to write.
* @throws IOException if an error occurred while writing the file.
*/
public static void writeTFW(File file, final AffineTransform tr) throws IOException {
final String suffix = toSuffixTFW(file);
final String name = file.getName();
final StringBuilder buffer = new StringBuilder(name);
final int s = name.lastIndexOf('.');
if (s >= 0) {
buffer.setLength(s + 1);
} else {
buffer.append('.');
}
file = new File(file.getParent(), buffer.append(suffix).toString());
writeTFW(new FileOutputStream(file), tr);
}
/**
* Writes the given affine transform as a TFW file.
*
* @param stream The output stream where to write. This stream will be closed by this method.
* @param tr The affine transform to write.
* @throws IOException if an error occurred while writing the file.
*/
public static void writeTFW(final OutputStream stream, final AffineTransform tr) throws IOException {
try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(stream, ENCODING))) {
final double[] matrix = new double[6];
tr.getMatrix(matrix);
for (int i=0; i<matrix.length; i++) {
out.write(String.valueOf(matrix[i]));
out.newLine();
}
}
}
/**
* Parses a TFW file and returns its content as an affine transform.
*
* @param file The file to parse. If it doesn't end with the {@code ".tfw"} suffix (for TIFF
* file) or {@code ".jgw"} suffix (for JPEG file), then the suffix of the given file
* will be replaced by the appropriate suffix.
* @return The TFW file content as an affine transform.
* @throws IOException If an error occurred while parsing the file, including
* errors while parsing the numbers.
*/
public static AffineTransform parseTFW(Path file) throws IOException {
file = toSupportFile(file, toSuffixTFW(file), true);
if (!Files.isRegularFile(file)) {
// Formats our own error message instead of the JSE one in order to localize it.
throw new FileNotFoundException(Errors.format(Errors.Keys.FileDoesNotExist_1, file.getFileName().toString()));
}
return parseTFW(Files.newInputStream(file), file.getFileName().toString());
}
/**
* Parses a TFW file and returns its content as an affine transform.
*
* @param input The input stream of the file to parse. Will be closed by this method.
* @param filename The name of the file being parsed. Used only for formatting error message.
* Can be a {@link String}, {@link File}, {@link URL} or {@link URI}.
* @return The TFW file content as an affine transform.
* @throws IOException If an error occurred while parsing the file, including
* errors while parsing the numbers.
*
* @since 3.07
*/
public static AffineTransform parseTFW(final InputStream input, final Object filename) throws IOException {
final double[] m;
int count;
try (LineNumberReader in = new LineNumberReader(new InputStreamReader(input, ENCODING))) {
m = new double[6];
count = 0;
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (!line.isEmpty() && line.charAt(0) != '#') {
if (count >= m.length) {
throw new ContentFormatException(Errors.format(Errors.Keys.FileHasTooManyData));
}
try {
m[count++] = Double.parseDouble(line);
} catch (NumberFormatException e) {
throw new ContentFormatException(Errors.format(Errors.Keys.IllegalLineInFile_2,
IOUtilities.filename(filename), in.getLineNumber()), e);
}
}
}
}
if (count != m.length) {
throw new EOFException(Errors.format(Errors.Keys.EndOfDataFile));
}
return new AffineTransform(m);
}
}