// tokenizes a string for PBM and PGM headers and plain PBM and PGM data.  If oneChar is true,
  // then
  // rather than parsing a whole string, a single character is read (not including whitespace or
  // comments)
  static String tokenizeString(InputStream stream, boolean oneChar) throws IOException {
    final int EOF = -1;

    StringBuilder b = new StringBuilder();
    int c;
    boolean inComment = false;

    while (true) {
      c = stream.read();
      if (c == EOF)
        throw new IOException(
            "Stream ended prematurely, before table reading was completed."); // no more tokens
      else if (inComment) {
        if (c == '\r' || c == '\n') // escape the comment
        inComment = false;
        else {
        } // do nothing
      } else if (Character.isWhitespace((char) c)) {
      } // do nothing
      else if (c == '#') // start of a comment
      {
        inComment = true;
      } else // start of a string
      {
        b.append((char) c);
        break;
      }
    }

    if (oneChar) return b.toString();

    // at this point we have a valid string.  We read until whitespace or a #
    while (true) {
      c = stream.read();
      if (c == EOF) break;
      else if (c == '#') // start of comment, read until a '\n'
      {
        while (true) {
          c = stream.read(); // could hit EOF, which is fine
          if (c == EOF) break;
          else if (c == '\r' || c == '\n') break;
        }
        // break;   // comments are not delimiters
      } else if (Character.isWhitespace((char) c)) break;
      else b.append((char) c);
    }
    return b.toString();
  }
  // Loads raw PGM files after the first-line header is stripped
  static int[][] loadRawPGM(InputStream stream) throws IOException {
    int width = tokenizeInt(stream);
    int height = tokenizeInt(stream);
    int maxVal = tokenizeInt(stream);
    if (width < 0) throw new IOException("Invalid width: " + width);
    if (height < 0) throw new IOException("Invalid height: " + height);
    if (maxVal <= 0) throw new IOException("Invalid maximum value: " + maxVal);

    // this single whitespace character will have likely already been consumed by reading maxVal
    // stream.read();  // must be a whitespace

    int[][] field = new int[width][height];
    for (int i = 0; i < height; i++)
      for (int j = 0; j < width; j++) {
        if (maxVal < 256) // one byte
        field[j][i] = stream.read();
        else if (maxVal < 65536) // two bytes
        field[j][i] =
              (stream.read() << 8) & stream.read(); // two bytes, most significant byte first
        else if (maxVal < 16777216) // three bytes -- this is nonstandard
        field[j][i] =
              (stream.read() << 16)
                  & (stream.read() << 8)
                  & stream.read(); // three bytes, most significant byte first
        else // four bytes -- this is nonstandard
        field[j][i] =
              (stream.read() << 24)
                  & (stream.read() << 16)
                  & (stream.read() << 8)
                  & stream.read(); // three bytes, most significant byte first
      }

    return field;
  }
  // Loads raw PBM files after the first-line header is stripped
  static int[][] loadRawPBM(InputStream stream) throws IOException {
    int width = tokenizeInt(stream);
    int height = tokenizeInt(stream);
    if (width < 0) throw new IOException("Invalid width in PBM: " + width);
    if (height < 0) throw new IOException("Invalid height in PBM: " + height);

    // this single whitespace character will have likely already been consumed by reading height
    // stream.read();  // must be a whitespace

    int[][] field = new int[width][height];
    for (int i = 0; i < height; i++) {
      int data = 0;
      int count = 0;
      for (int j = 0; j < width; j++) {
        if (count == 0) {
          data = stream.read();
          count = 8;
        }
        count--;
        field[j][i] = (data >> count) & 0x1;
      }
    }

    return field;
  }