From 130460fa3cf2cd68418df5d48523ee30bec311bd Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 20 Jul 2023 14:40:29 +0200 Subject: [PATCH] Handle spaces in map file names in MTL --- src/main/java/de/javagl/obj/MtlReader.java | 181 ++++++++++++++---- src/main/java/de/javagl/obj/Utils.java | 84 +++++++- .../java/de/javagl/obj/TestMtlReader.java | 81 +++++++- .../java/de/javagl/obj/TestMtlWriter.java | 2 +- .../resources/mtlWithSpaceInMapFileNames.mtl | 11 ++ src/test/resources/mtlWithWhitespace.mtl | 5 +- 6 files changed, 312 insertions(+), 52 deletions(-) create mode 100644 src/test/resources/mtlWithSpaceInMapFileNames.mtl diff --git a/src/main/java/de/javagl/obj/MtlReader.java b/src/main/java/de/javagl/obj/MtlReader.java index a9c2770..af66000 100644 --- a/src/main/java/de/javagl/obj/MtlReader.java +++ b/src/main/java/de/javagl/obj/MtlReader.java @@ -271,10 +271,12 @@ else if (command.equalsIgnoreCase("anisor")) // Texture map definitions else { - readTextureMap(mtl, command, tokens); + String textureOptionsString = Utils.consumeNextToken(line); + readTextureMap(mtl, command, textureOptionsString); } } + /** * Process the line of an MTL file that is supposed to contain a * texture map definition, and write the resulting texture @@ -286,166 +288,267 @@ else if (command.equalsIgnoreCase("anisor")) * * @param mtl The {@link Mtl} * @param command The command at the beginning of the line - * @param tokens The tokens that have been created from the line + * @param textureOptionsString The texture options, i.e. the part of + * the line after the command * @throws IOException If an IO error occurs */ private static void readTextureMap( - Mtl mtl, String command, Queue tokens) + Mtl mtl, String command, String textureOptionsString) throws IOException { if (command.equalsIgnoreCase("map_Ka")) { - mtl.setMapKaOptions(readTextureOptions(tokens)); + mtl.setMapKaOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Kd")) { - mtl.setMapKdOptions(readTextureOptions(tokens)); + mtl.setMapKdOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Ks")) { - mtl.setMapKsOptions(readTextureOptions(tokens)); + mtl.setMapKsOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_d")) { - mtl.setMapDOptions(readTextureOptions(tokens)); + mtl.setMapDOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Ns")) { - mtl.setMapNsOptions(readTextureOptions(tokens)); + mtl.setMapNsOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("bump") || command.equalsIgnoreCase("map_bump")) { - mtl.setBumpOptions(readTextureOptions(tokens)); + mtl.setBumpOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("disp")) { - mtl.setDispOptions(readTextureOptions(tokens)); + mtl.setDispOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("decal")) { - mtl.setDecalOptions(readTextureOptions(tokens)); + mtl.setDecalOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("refl")) { - TextureOptions refl = readTextureOptions(tokens); + TextureOptions refl = readTextureOptions(textureOptionsString); mtl.getReflOptions().add(refl); } // Texture map definitions for PBR else if (command.equalsIgnoreCase("map_Pr")) { - mtl.setMapPrOptions(readTextureOptions(tokens)); + mtl.setMapPrOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Pm")) { - mtl.setMapPmOptions(readTextureOptions(tokens)); + mtl.setMapPmOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Ps")) { - mtl.setMapPsOptions(readTextureOptions(tokens)); + mtl.setMapPsOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("map_Ke")) { - mtl.setMapKeOptions(readTextureOptions(tokens)); + mtl.setMapKeOptions(readTextureOptions(textureOptionsString)); } else if (command.equalsIgnoreCase("norm")) { - mtl.setNormOptions(readTextureOptions(tokens)); + mtl.setNormOptions(readTextureOptions(textureOptionsString)); } } /** - * Process the tokens in the given queue and construct a - * {@link TextureOptions} object from them + * Process the given texture options string and construct a + * {@link TextureOptions} object from it * - * @param tokens The input token + * @param textureOptionsString The texture options, i.e. the part of + * the line after the command * @return The {@link TextureOptions} * @throws IOException If an IO error occurs */ - static TextureOptions readTextureOptions(Queue tokens) + static TextureOptions readTextureOptions(String textureOptionsString) throws IOException { + String s = textureOptionsString; DefaultTextureOptions textureOptions = new DefaultTextureOptions(); - while (!tokens.isEmpty()) + while (true) { - String optionName = tokens.poll(); + // If the next token is one of the known options, then consume + // the token. The next tokens will then contain the value(s) + // of the options. These values are parsed, consume, and their + // values are assigned to the texture options + String optionName = Utils.extractNextToken(s); if (optionName.equalsIgnoreCase("-blendu")) { - boolean value = Utils.parseBoolean(tokens.poll()); + s = Utils.consumeNextToken(s); + boolean value = Utils.parseNextBoolean(s); + s = Utils.consumeNextToken(s); textureOptions.setBlendu(value); } else if (optionName.equalsIgnoreCase("-blendv")) { - boolean value = Utils.parseBoolean(tokens.poll()); + s = Utils.consumeNextToken(s); + boolean value = Utils.parseNextBoolean(s); + s = Utils.consumeNextToken(s); textureOptions.setBlendv(value); } else if (optionName.equalsIgnoreCase("-boost")) { - float value = Utils.parseFloat(tokens.poll()); + s = Utils.consumeNextToken(s); + float value = Utils.parseNextFloat(s); + s = Utils.consumeNextToken(s); textureOptions.setBoost(value); } else if (optionName.equalsIgnoreCase("-cc")) { - boolean value = Utils.parseBoolean(tokens.poll()); + s = Utils.consumeNextToken(s); + boolean value = Utils.parseNextBoolean(s); + s = Utils.consumeNextToken(s); textureOptions.setCc(value); } else if (optionName.equalsIgnoreCase("-mm")) { - float base = Utils.parseFloat(tokens.poll()); - float gain = Utils.parseFloat(tokens.poll()); + s = Utils.consumeNextToken(s); + float base = Utils.parseNextFloat(s); + s = Utils.consumeNextToken(s); + float gain = Utils.parseNextFloat(s); + s = Utils.consumeNextToken(s); textureOptions.setMm(base, gain); } else if (optionName.equalsIgnoreCase("-o")) { - Float[] values = Utils.parseFloats(tokens, 3); + s = Utils.consumeNextToken(s); + + Float[] values = new Float[3]; + + // The u value is required + String su = Utils.extractNextToken(s); + s = Utils.consumeNextToken(s); + values[0] = Utils.parseFloat(su); + + // The v/w values are optional. Only parse and consume + // the next tokens if they are float values. + String sv = Utils.extractNextToken(s); + if (Utils.isFloat(sv)) + { + values[1] = Utils.parseFloat(sv); + s = Utils.consumeNextToken(s); + String sw = Utils.extractNextToken(s); + if (Utils.isFloat(sw)) + { + values[2] = Utils.parseFloat(sw); + s = Utils.consumeNextToken(s); + } + } textureOptions.setO(values[0], values[1], values[2]); } else if (optionName.equalsIgnoreCase("-s")) { - Float[] values = Utils.parseFloats(tokens, 3); + s = Utils.consumeNextToken(s); + + Float[] values = new Float[3]; + + // The u value is required + String su = Utils.extractNextToken(s); + s = Utils.consumeNextToken(s); + values[0] = Utils.parseFloat(su); + + // The v/w values are optional. Only parse and consume + // the next tokens if they are float values. + String sv = Utils.extractNextToken(s); + if (Utils.isFloat(sv)) + { + values[1] = Utils.parseFloat(sv); + s = Utils.consumeNextToken(s); + String sw = Utils.extractNextToken(s); + if (Utils.isFloat(sw)) + { + values[2] = Utils.parseFloat(sw); + s = Utils.consumeNextToken(s); + } + } textureOptions.setS(values[0], values[1], values[2]); } else if (optionName.equalsIgnoreCase("-t")) { - Float[] values = Utils.parseFloats(tokens, 3); + s = Utils.consumeNextToken(s); + + Float[] values = new Float[3]; + + // The u value is required + String su = Utils.extractNextToken(s); + s = Utils.consumeNextToken(s); + values[0] = Utils.parseFloat(su); + + // The v/w values are optional. Only parse and consume + // the next tokens if they are float values. + String sv = Utils.extractNextToken(s); + if (Utils.isFloat(sv)) + { + values[1] = Utils.parseFloat(sv); + s = Utils.consumeNextToken(s); + String sw = Utils.extractNextToken(s); + if (Utils.isFloat(sw)) + { + values[2] = Utils.parseFloat(sw); + s = Utils.consumeNextToken(s); + } + } textureOptions.setT(values[0], values[1], values[2]); } else if (optionName.equalsIgnoreCase("-texres")) { - float value = Utils.parseFloat(tokens.poll()); + s = Utils.consumeNextToken(s); + float value = Utils.parseNextFloat(s); + s = Utils.consumeNextToken(s); textureOptions.setTexres(value); } else if (optionName.equalsIgnoreCase("-clamp")) { - boolean value = Utils.parseBoolean(tokens.poll()); + s = Utils.consumeNextToken(s); + boolean value = Utils.parseNextBoolean(s); + s = Utils.consumeNextToken(s); textureOptions.setClamp(value); } else if (optionName.equalsIgnoreCase("-bm")) { - float value = Utils.parseFloat(tokens.poll()); + s = Utils.consumeNextToken(s); + float value = Utils.parseNextFloat(s); + s = Utils.consumeNextToken(s); textureOptions.setBm(value); } else if (optionName.equalsIgnoreCase("-imfchan")) { - String value = tokens.poll(); + s = Utils.consumeNextToken(s); + String value = Utils.extractNextToken(s); + s = Utils.consumeNextToken(s); textureOptions.setImfchan(value); } else if (optionName.equalsIgnoreCase("-type")) { - String value = tokens.poll(); + s = Utils.consumeNextToken(s); + String value = Utils.extractNextToken(s); + s = Utils.consumeNextToken(s); textureOptions.setType(value); } else { - textureOptions.setFileName(optionName); + // The current token is not one of the known options. + // Treat the remaining part of the options (including + // the current token) as the "file name" + textureOptions.setFileName(s); + break; } } return textureOptions; } - + + + + /** * Private constructor to prevent instantiation diff --git a/src/main/java/de/javagl/obj/Utils.java b/src/main/java/de/javagl/obj/Utils.java index cb2d88d..f459745 100644 --- a/src/main/java/de/javagl/obj/Utils.java +++ b/src/main/java/de/javagl/obj/Utils.java @@ -27,6 +27,8 @@ package de.javagl.obj; import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; import java.util.Queue; import java.util.StringTokenizer; @@ -93,7 +95,7 @@ static float parseFloat(String s) throws IOException * @return Whether the string is a float value. If the given string is * null, then false is returned. */ - private static boolean isFloat(String s) + static boolean isFloat(String s) { if (s == null) { @@ -109,6 +111,84 @@ private static boolean isFloat(String s) return false; } } + + /** + * Extract the next token from the given string, parse it as a float + * value, and return the result + * + * @param s The input string + * @return The parsed token value + * @throws IOException If the string does not contain a valid float value + */ + static float parseNextFloat(String s) throws IOException + { + String token = extractNextToken(s); + return parseFloat(token); + } + + /** + * Extract the next token from the given string, parse it as a boolean + * value, and return the result + * + * @param s The input string + * @return The parsed token value + * @throws IOException If the string does not contain a valid boolean value + */ + static boolean parseNextBoolean(String s) throws IOException + { + String token = extractNextToken(s); + return parseBoolean(token); + } + + /** + * Extract the next token from the given string. + * + * The next token starts at the first non-whitespace character, and + * ends at the first character that is either a whitespace or a + * line feed or carriage return. + * + * @param s The input string + * @return The next token + */ + static String extractNextToken(String s) + { + String t = s.trim(); + for (int i = 0; i < t.length(); i++) + { + char c = t.charAt(i); + if (c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '\f') + { + return t.substring(0, i).trim(); + } + } + return ""; + } + + /** + * Consume the next token from the given string, and return the remaining + * string, excluding spaces. + * + * The next token starts at the first non-whitespace character, and + * ends at the first character that is either a whitespace or a + * line feed or carriage return. + * + * @param s The input string + * @return The string after the next token, trimmed + */ + static String consumeNextToken(String s) + { + String t = s.trim(); + for (int i = 0; i < t.length(); i++) + { + char c = t.charAt(i); + if (c == ' ' || c == '\n' || c == '\t' || c == '\r' || c == '\f') + { + return t.substring(i + 1, t.length()).trim(); + } + } + return t; + } + /** * Parse up to max float values from the given tokens. @@ -155,7 +235,7 @@ static boolean parseBoolean(String s) } return false; } - + /** * Parse an int from the given string, wrapping number format diff --git a/src/test/java/de/javagl/obj/TestMtlReader.java b/src/test/java/de/javagl/obj/TestMtlReader.java index 27a9890..aca0585 100644 --- a/src/test/java/de/javagl/obj/TestMtlReader.java +++ b/src/test/java/de/javagl/obj/TestMtlReader.java @@ -3,8 +3,6 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; -import java.util.Arrays; -import java.util.LinkedList; import java.util.List; import org.junit.Test; @@ -51,8 +49,43 @@ public void readMtlWithWhitespace() assertEquals(500, mtl.getNs(), FLOAT_ERROR); assertEquals(1.0f, mtl.getD(), FLOAT_ERROR); assertEquals("texture.png", mtl.getMapKd()); + assertEquals(new DefaultFloatTuple(2,2,2), + mtl.getMapKdOptions().getS()); + } + @Test + public void readMtlWithSpaceInMapFileNames() + throws IOException + { + List mtls = MtlReader.read(getClass().getResourceAsStream( + "/mtlWithSpaceInMapFileNames.mtl")); + + assertEquals(1, mtls.size()); + + Mtl mtl = mtls.get(0); + assertEquals("material0", mtl.getName()); + assertEquals(new DefaultFloatTuple(1,0,0), mtl.getKa()); + assertEquals(new DefaultFloatTuple(1,1,0), mtl.getKd()); + assertEquals(new DefaultFloatTuple(1,1,1), mtl.getKs()); + assertEquals(500, mtl.getNs(), FLOAT_ERROR); + assertEquals(123.0f, mtl.getD(), FLOAT_ERROR); + + assertEquals(Boolean.TRUE, mtl.getMapKdOptions().isCc()); + assertEquals("file name with spaces.png", mtl.getMapKd()); + + assertEquals(new DefaultFloatTuple(2,2,2), + mtl.getMapKaOptions().getS()); + assertEquals("directory name with spaces/file name with spaces.png", + mtl.getMapKa()); + + assertEquals(new DefaultFloatTuple(2,2,1), + mtl.getMapKsOptions().getS()); + assertEquals("/ another file name .png", + mtl.getMapKs()); + + } + @Test public void readMtlWithBrokenLines() throws IOException @@ -94,9 +127,19 @@ public void readTextureOptionsWithAllOptions() "-type", "sphere", "texture.png" }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) + { + if (i > 0) + { + sb.append(" "); + } + sb.append(tokens[i]); + } + String textureOptionsString = sb.toString(); - TextureOptions options = MtlReader.readTextureOptions( - new LinkedList(Arrays.asList(tokens))); + TextureOptions options = + MtlReader.readTextureOptions(textureOptionsString); assertEquals(Boolean.FALSE, options.isBlendu()); assertEquals(Boolean.FALSE, options.isBlendv()); @@ -140,8 +183,19 @@ public void readTextureOptionsWithSingleOriginOffsetValue() { "-o", "0.1", "texture.png" }; - TextureOptions options = MtlReader.readTextureOptions( - new LinkedList(Arrays.asList(tokens))); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) + { + if (i > 0) + { + sb.append(" "); + } + sb.append(tokens[i]); + } + String textureOptionsString = sb.toString(); + + TextureOptions options = + MtlReader.readTextureOptions(textureOptionsString); assertEquals(0.1f, options.getO().getX(), FLOAT_ERROR); assertEquals(0.0f, options.getO().getY(), FLOAT_ERROR); @@ -156,8 +210,19 @@ public void readTextureOptionsWithDoubleOriginOffsetValue() { "-o", "0.1", "0.2", "texture.png" }; - TextureOptions options = MtlReader.readTextureOptions( - new LinkedList(Arrays.asList(tokens))); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) + { + if (i > 0) + { + sb.append(" "); + } + sb.append(tokens[i]); + } + String textureOptionsString = sb.toString(); + + TextureOptions options = + MtlReader.readTextureOptions(textureOptionsString); assertEquals(0.1f, options.getO().getX(), FLOAT_ERROR); assertEquals(0.2f, options.getO().getY(), FLOAT_ERROR); diff --git a/src/test/java/de/javagl/obj/TestMtlWriter.java b/src/test/java/de/javagl/obj/TestMtlWriter.java index ce5506f..67a2bd2 100644 --- a/src/test/java/de/javagl/obj/TestMtlWriter.java +++ b/src/test/java/de/javagl/obj/TestMtlWriter.java @@ -62,7 +62,7 @@ public void writePbrMtl() MtlWriter.write(mtls, baos); String outputString = new String(baos.toByteArray()); - System.out.println(outputString); + //System.out.println(outputString); assertEquals(inputString, outputString); } diff --git a/src/test/resources/mtlWithSpaceInMapFileNames.mtl b/src/test/resources/mtlWithSpaceInMapFileNames.mtl new file mode 100644 index 0000000..81726a6 --- /dev/null +++ b/src/test/resources/mtlWithSpaceInMapFileNames.mtl @@ -0,0 +1,11 @@ +# The texture map definitions contain directory- and file names +# that contain spaces +newmtl material0 +Ka 1.0 0.0 0.0 +Kd 1.0 1.0 0.0 +Ks 1.0 1.0 1.0 +Ns 500.0 +map_Kd -cc on file name with spaces.png +map_Ka -s 2 2 2 directory name with spaces/file name with spaces.png +map_Ks -s 2 2 / another file name .png +d 123.0 \ No newline at end of file diff --git a/src/test/resources/mtlWithWhitespace.mtl b/src/test/resources/mtlWithWhitespace.mtl index 892d44c..279fd2e 100644 --- a/src/test/resources/mtlWithWhitespace.mtl +++ b/src/test/resources/mtlWithWhitespace.mtl @@ -1,9 +1,10 @@ # Some lines have leading and trailing whitespace, -# in form of space characters or tabs +# in form of space characters or tabs, and tabs +# between elements of the texture map options newmtl material0 Ka 1.0 0.0 0.0 Kd 1.0 1.0 0.0 Ks 1.0 1.0 1.0 Ns 500.0 - map_Kd texture.png + map_Kd -s 2 2 2 texture.png d 1.0