dragon66 / icafe

Java library for reading, writing, converting and manipulating images and metadata
Eclipse Public License 1.0
203 stars 58 forks source link

TIF - ByteOrder.LITTLE_ENDIAN not respected #79

Closed ben-manes closed 5 years ago

ben-manes commented 5 years ago

I am using TIFFTweaker.writeMultipageTIFF with TIFFOptions set to LITTLE_ENDIAN and CCITTFAX4. Unfortunately the resulting image is big endian, as confirmed by ImageMagik ($ identify -verbose out.tif | grep Endianess). Toggling the setting doesn't seem to have an effect.

Since the source jar isn't being published, I can't step through it to debug. It does naively look correct in TIFFWriter, but doesn't seem to be working.

ben-manes commented 5 years ago

I think it might be due to the header?

https://github.com/dragon66/icafe/blob/427b2fc778c2ba27cc1a2aae90a460e94e8e5925/src/com/icafe4j/image/tiff/TIFFTweaker.java#L3394-L3402

ben-manes commented 5 years ago

That did it! I think you should be setting this for the caller, as it requires setting the writer manually.

rout.setWriteStrategy((settings.byteOrder() == BIG_ENDIAN)
    ? WriteStrategyMM.getInstance()
    : WriteStrategyII.getInstance());
$ identify -verbose out.tif | grep Endian
  Endianess: LSB
dragon66 commented 5 years ago

@ben-manes The method writeHeader(RandomAccessOutputStream rout) itself is called by different methods. Most of the methods already set the byte order before calling this method.

For this reason, I believe writeMultipageTIFF(RandomAccessOutputStream rout, ImageParam[] imageParam, BufferedImage ... images) method should be the right place to set the byte order before calling writeHeader(RandomAccessOutputStream rout).

One catch here is multiple page TIFF only allows one byte order on the image level, not page level. As we allow each page to be associated with an ImageParam, theoretically, the user can set different byte order for different pages but this is not supported by multipage TIFF.

What I can do is to grab the first ImageParam from the ImageParam array and use the byte order in there.

ben-manes commented 5 years ago

That sounds reasonable to me. Thanks!

dragon66 commented 5 years ago

I also added an overloaded prepareForWrite(RandomAccessOutputStream, ByteOrder) method which takes a second argument of type ByteOrder. This way, writing multipage TIFF page by page will also respect ByteOrder. The old single argument method will by default take ByteOrder.BIG_ENDIAN.

ben-manes commented 5 years ago

Thanks! If I use prepareForWrite with writeMultipageTIFF, won't it write the header twice?

Just for fyi, my usage currently looks like:

public final class MultipageTiff {

  private MultipageTiff() {}

  /** Creates a TIFF with a page per image. */
  public static void create(TiffSettings settings) {
    try (var fileOutput = Files.newOutputStream(settings.destination());
         var rout = new FileCacheRandomAccessOutputStream(fileOutput)) {
      BufferedImage[] bufferedImages = new BufferedImage[settings.images().size()];
      for (int i = 0; i < settings.images().size(); i++) {
        bufferedImages[i] = ImageIO.read(settings.images().get(i).toFile());
      }

      TIFFOptions tiffOptions = new TIFFOptions();
      tiffOptions.setTiffCompression(settings.compression());
      tiffOptions.setXResolution(settings.xResolution());
      tiffOptions.setYResolution(settings.yResolution());
      tiffOptions.setByteOrder(settings.byteOrder());
      tiffOptions.setJPEGQuality(100);

      ImageParam imageSetting = ImageParam.getBuilder()
          .colorType(settings.colorType())
          .imageOptions(tiffOptions)
          .build();
      rout.setWriteStrategy((settings.byteOrder() == BIG_ENDIAN)
          ? WriteStrategyMM.getInstance()
          : WriteStrategyII.getInstance());
      TIFFTweaker.writeMultipageTIFF(rout, new ImageParam[] { imageSetting }, bufferedImages);
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

  @AutoValue @AutoBuilder
  public static abstract class TiffSettings {
    public abstract Path destination();
    public abstract List<Path> images();
    public abstract Compression compression();
    public abstract ImageColorType colorType();
    public abstract int xResolution();
    public abstract int yResolution();
    public abstract ByteOrder byteOrder();

    public static MultipageTiff_TiffSettings_Builder group4() {
      return MultipageTiff_TiffSettings_Builder.builder()
          .compression(CCITTFAX4)
          .byteOrder(BIG_ENDIAN)
          .colorType(BILEVEL)
          .xResolution(200)
          .yResolution(200);
    }

    public static MultipageTiff_TiffSettings_Builder color() {
      return MultipageTiff_TiffSettings_Builder.builder()
          .colorType(FULL_COLOR)
          .byteOrder(BIG_ENDIAN)
          .compression(LZW)
          .xResolution(72)
          .yResolution(72);
    }
  }
}
dragon66 commented 5 years ago

prepareForWrite() is supposed to be used along with writePage() and finishWrite() not together with any of the writeMultipageTIFF(). This is to save memory.

You call prepareForWrite() first. Then writePage() one or multiple times depending on how many pages you want to write. Then finally wrap up calling finishWrite().

A similar work flow exists for inserting pages into existing TIFF.

Refer to this issue: https://github.com/dragon66/icafe/issues/25