View Javadoc
1   package nom.tam.image.compression;
2   
3   import java.io.IOException;
4   import java.nio.Buffer;
5   import java.nio.ByteBuffer;
6   import java.util.ArrayList;
7   import java.util.Arrays;
8   import java.util.List;
9   import java.util.logging.Logger;
10  
11  /*
12   * #%L
13   * nom.tam FITS library
14   * %%
15   * Copyright (C) 2004 - 2024 nom-tam-fits
16   * %%
17   * This is free and unencumbered software released into the public domain.
18   *
19   * Anyone is free to copy, modify, publish, use, compile, sell, or
20   * distribute this software, either in source code form or as a compiled
21   * binary, for any purpose, commercial or non-commercial, and by any
22   * means.
23   *
24   * In jurisdictions that recognize copyright laws, the author or authors
25   * of this software dedicate any and all copyright interest in the
26   * software to the public domain. We make this dedication for the benefit
27   * of the public at large and to the detriment of our heirs and
28   * successors. We intend this dedication to be an overt act of
29   * relinquishment in perpetuity of all present and future rights to this
30   * software under copyright law.
31   *
32   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
35   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
36   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
37   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
38   * OTHER DEALINGS IN THE SOFTWARE.
39   * #L%
40   */
41  
42  import nom.tam.fits.FitsException;
43  import nom.tam.fits.Header;
44  import nom.tam.fits.compression.algorithm.api.ICompressOption;
45  import nom.tam.fits.compression.algorithm.api.ICompressorControl;
46  import nom.tam.fits.compression.algorithm.quant.QuantizeOption;
47  import nom.tam.fits.compression.algorithm.rice.RiceCompressOption;
48  import nom.tam.fits.compression.provider.CompressorProvider;
49  import nom.tam.fits.header.Compression;
50  import nom.tam.fits.header.Standard;
51  import nom.tam.image.ImageTiler;
52  import nom.tam.image.StandardImageTiler;
53  import nom.tam.image.compression.hdu.CompressedImageHDU;
54  import nom.tam.util.ArrayDataOutput;
55  import nom.tam.util.ArrayFuncs;
56  import nom.tam.util.type.ElementType;
57  
58  /**
59   * Class to extract individually compressed tiles from a compressed image. This class supports the FITS 3.0 standard and
60   * up, and will stream the results to a provided {@link nom.tam.util.ArrayDataOutput}.
61   * 
62   * @see nom.tam.image.compression.hdu.CompressedImageHDU
63   */
64  public class CompressedImageTiler implements ImageTiler {
65      private static final Logger LOGGER = Logger.getLogger(CompressedImageTiler.class.getName());
66  
67      static final int DEFAULT_BLOCK_SIZE = 32;
68  
69      /**
70       * Increment the offset within the position array. Note that we never look at the last index since we copy data a
71       * block at a time and not byte by byte.
72       *
73       * @param  start   The starting corner values.
74       * @param  current The current offsets.
75       * @param  lengths The desired dimensions of the subset.
76       * @param  steps   The amount to increment by.
77       *
78       * @return         <code>true</code> if the current array was changed
79       */
80      static boolean incrementPosition(int[] start, int[] current, int[] lengths, int[] steps) {
81          for (int i = start.length - 2; i >= 0; i--) {
82              if (current[i] - start[i] < lengths[i] - steps[i]) {
83                  current[i] += steps[i];
84                  if (start.length - 1 - (i + 1) >= 0) {
85                      System.arraycopy(start, i + 1, current, i + 1, start.length - 1 - (i + 1));
86                  }
87                  return true;
88              }
89          }
90          return false;
91      }
92  
93      /**
94       * Easily testable static function to ensure the next requested segment of a Tile fits.
95       *
96       * @param  position  The current position.
97       * @param  length    The requested length.
98       * @param  dimension The dimension of the current axis.
99       *
100      * @return           True if valid, False otherwise.
101      */
102     static boolean isValidSegment(final int position, final int length, final int dimension) {
103         return position + length >= 0 && position < dimension;
104     }
105 
106     private final CompressedImageHDU compressedImageHDU;
107 
108     private final List<String> columnNames = new ArrayList<>();
109 
110     /**
111      * Only constructor. This will pull commonly accessed elements (header, data) from the HDU.
112      *
113      * @param compressedImageHDU The compressed Image HDU.
114      */
115     public CompressedImageTiler(final CompressedImageHDU compressedImageHDU) {
116         this.compressedImageHDU = compressedImageHDU;
117         init();
118     }
119 
120     void init() {
121         final int columnCount = compressedImageHDU.getData().getNCols();
122         final Header header = compressedImageHDU.getHeader();
123 
124         for (int index = 0; index < columnCount; index++) {
125             final String ttype = header.getStringValue(Standard.TTYPEn.n(index + 1));
126             if (ttype != null) {
127                 addColumn(ttype.trim());
128             }
129         }
130     }
131 
132     void addColumn(final String column) {
133         columnNames.add(column);
134     }
135 
136     /**
137      * Fill the subset.
138      *
139      * @param  output          The stream to be written to.
140      * @param  imageDimensions The pixel dimensions of the full image (uncompressed and before slicing).
141      * @param  corners         The pixel indices of the corner of the image.
142      * @param  lengths         The pixel dimensions of the subset.
143      * @param  steps           The pixel amount between values.
144      *
145      * @throws IOException     if the underlying stream failed
146      * @throws FitsException   if any header values cannot be retrieved, or the dimensions are incorrect.
147      */
148     void getTile(final ArrayDataOutput output, final int[] imageDimensions, final int[] corners, final int[] lengths,
149             final int[] steps) throws IOException, FitsException {
150 
151         final int n = imageDimensions.length;
152         final int[] posits = new int[n];
153 
154         // This is the step value for this segment (current row)
155         final int segmentStepValue = steps[n - 1];
156 
157         final int segment = lengths[n - 1];
158 
159         // The primitive base class of this image's data.
160         final Class<?> base = getBaseType().primitiveClass();
161 
162         System.arraycopy(corners, 0, posits, 0, n);
163         final int[] tileDimensions = ArrayFuncs.reverseIndices(getTileDimensions());
164 
165         do {
166             // This implies there is some overlap
167             // in the last index (in conjunction
168             // with other tests)
169             final int mx = imageDimensions.length - 1;
170             boolean validSegment = CompressedImageTiler.isValidSegment(posits[mx], lengths[mx], imageDimensions[mx]);
171 
172             if (validSegment) {
173                 int stepOffset = 0;
174                 int pixelsRead = 0;
175                 final int[] tileRowPositions = new int[n];
176                 System.arraycopy(posits, 0, tileRowPositions, 0, n);
177                 while (pixelsRead < segment) {
178                     final int[] tileOffsets = getTileOffsets(tileRowPositions, tileDimensions);
179                     // Multidimensional array
180                     final Object tileData = getDecompressedTileData(tileRowPositions, tileDimensions);
181                     final StandardImageTiler standardImageTiler = new StandardImageTiler(null, -1, tileDimensions, base) {
182                         @Override
183                         protected Object getMemoryImage() {
184                             return tileData;
185                         }
186                     };
187                     final int remaining = segment - pixelsRead;
188 
189                     // Apply any remaining steps that didn't get read from the last tile.
190                     tileOffsets[mx] += stepOffset;
191                     final int segmentLength = Math.min(segment, tileDimensions[mx] - tileOffsets[mx]);
192                     final int tileReadLength = Math.max(1, Math.min(remaining, segmentLength));
193 
194                     final int[] tileReadLengths = new int[tileDimensions.length];
195                     Arrays.fill(tileReadLengths, 1);
196                     tileReadLengths[tileReadLengths.length - 1] = tileReadLength;
197 
198                     final int[] tileSteps = new int[tileDimensions.length];
199                     Arrays.fill(tileSteps, 1);
200                     tileSteps[tileSteps.length - 1] = segmentStepValue;
201 
202                     // Slice out a 1-dimensional array as we're reading Pixels row by row.
203                     standardImageTiler.getTile(output, tileOffsets, tileReadLengths, tileSteps);
204                     final int unreadSteps = tileReadLength % segmentStepValue;
205 
206                     stepOffset = (unreadSteps > 0) ? segmentStepValue - unreadSteps : 0;
207                     pixelsRead += tileReadLength;
208                     tileRowPositions[mx] = tileRowPositions[mx] + tileReadLength;
209                 }
210             }
211         } while (CompressedImageTiler.incrementPosition(corners, posits, lengths, steps));
212         output.flush();
213     }
214 
215     /**
216      * Obtain the multidimensional decompressed array of values for the tile at the given position.
217      *
218      * @param  positions      The location to obtain the tile.
219      * @param  tileDimensions The N-dimensional array of a full tile.
220      *
221      * @return                N-dimensional array of values.
222      *
223      * @throws FitsException  For any header read errors.
224      */
225     Object getDecompressedTileData(final int[] positions, final int[] tileDimensions) throws FitsException {
226         final int compressedDataColumnIndex = columnNames.indexOf(Compression.COMPRESSED_DATA_COLUMN);
227         final int uncompressedDataColumnIndex = columnNames.indexOf(Compression.UNCOMPRESSED_DATA_COLUMN);
228         final int gZipCompressedDataColumnIndex = columnNames.indexOf(Compression.GZIP_COMPRESSED_DATA_COLUMN);
229         final Object[] row = getRow(positions, tileDimensions);
230         final Object decompressedArray;
231 
232         final byte[] compressedRowData = (byte[]) row[compressedDataColumnIndex];
233         if (compressedRowData.length > 0) {
234             decompressedArray = decompressRow(compressedDataColumnIndex, row);
235         } else if (gZipCompressedDataColumnIndex >= 0) {
236             decompressedArray = decompressRow(gZipCompressedDataColumnIndex, row);
237         } else if (uncompressedDataColumnIndex >= 0) {
238             decompressedArray = row[uncompressedDataColumnIndex];
239         } else {
240             throw new FitsException("Nothing in row to read: (" + Arrays.deepToString(row) + ").");
241         }
242 
243         return ArrayFuncs.curl(decompressedArray, tileDimensions);
244     }
245 
246     int[] getTileIndexes(final int[] pixelPositions, final int[] tileDimensions) {
247         final int[] tileIndexes = new int[pixelPositions.length];
248 
249         for (int i = 0; i < pixelPositions.length; i++) {
250             tileIndexes[i] = pixelPositions[i] / tileDimensions[i];
251         }
252 
253         return tileIndexes;
254     }
255 
256     /**
257      * Decompress the data at row <code>rowNumber</code> and column <code>columnIndex</code>.
258      *
259      * @param  columnIndex   The column containing the expected compressed data.
260      * @param  row           The desired row data.
261      *
262      * @return               Object array.
263      *
264      * @throws FitsException If there is no array, or it cannot be decompressed.
265      */
266     Object decompressRow(final int columnIndex, final Object[] row) throws FitsException {
267         final byte[] compressedRowData = (byte[]) row[columnIndex];
268 
269         // Decompress the row into pixel values.
270         final ByteBuffer compressed = ByteBuffer.wrap(compressedRowData);
271         compressed.rewind();
272 
273         try {
274             final Buffer tileBuffer = decompressIntoBuffer(row, compressed);
275             if (hasData(tileBuffer)) {
276                 return tileBuffer.array();
277             }
278             throw new FitsException("No tile available at column " + columnIndex + ": (" + Arrays.deepToString(row) + ")");
279         } catch (IllegalStateException illegalStateException) {
280             // This can sometimes happen if the compressed data (or surrounding data) are of incorrect length. The
281             // input is invalid in that case.
282             LOGGER.severe(
283                     "Unable to decompress row data from column " + columnIndex + ": (" + Arrays.deepToString(row) + ")");
284             throw new FitsException(illegalStateException.getMessage(), illegalStateException);
285         }
286     }
287 
288     /**
289      * Decompress the given ByteBuffer into a primitive class based Buffer.
290      *
291      * @param  row        The row array data.
292      * @param  compressed The compressed data.
293      *
294      * @return            Buffer instance. Never null.
295      */
296     Buffer decompressIntoBuffer(final Object[] row, final ByteBuffer compressed) {
297         final ElementType<Buffer> bufferElementType = getBaseType();
298         final Buffer tileBuffer = bufferElementType.newBuffer(getTileSize());
299         tileBuffer.rewind();
300         final ICompressorControl compressorControl = getCompressorControl(getBaseType());
301         final ICompressOption option = initCompressionOption(compressorControl.option(), bufferElementType.size());
302         initRowOption(option, row);
303         compressorControl.decompress(compressed, tileBuffer, option);
304 
305         tileBuffer.rewind();
306         return tileBuffer;
307     }
308 
309     ICompressorControl getCompressorControl(final ElementType<? extends Buffer> elementType) {
310         return CompressorProvider.findCompressorControl(getQuantizAlgorithmName(), getCompressionAlgorithmName(),
311                 elementType.primitiveClass());
312     }
313 
314     ICompressOption initCompressionOption(final ICompressOption option, final int bytePix) {
315         if (option instanceof RiceCompressOption) {
316             ((RiceCompressOption) option).setBlockSize(getBlockSize());
317             ((RiceCompressOption) option).setBytePix(bytePix);
318         } else if (option instanceof QuantizeOption) {
319             initCompressionOption(((QuantizeOption) option).getCompressOption(), bytePix);
320         }
321 
322         option.setTileHeight(getTileHeight()).setTileWidth(getTileWidth());
323 
324         return option;
325     }
326 
327     ElementType<Buffer> getBaseType() {
328         final int zBitPix = getZBitPix();
329         final ElementType<Buffer> bufferElementType = ElementType.forBitpix(zBitPix);
330         if (bufferElementType == null) {
331             return ElementType.forNearestBitpix(zBitPix);
332         }
333         return bufferElementType;
334     }
335 
336     /**
337      * Ensure the Buffer has data that can be used. Tests can override.
338      *
339      * @param  buffer The buffer to check.
340      *
341      * @return        True if there is an array, even an empty one. False otherwise.
342      */
343     boolean hasData(final Buffer buffer) {
344         return buffer.hasArray();
345     }
346 
347     /**
348      * Obtain the row for the given number. Tests can override this to alleviate the need to create an HDU.
349      *
350      * @param  positions      The corners of the desired tile.
351      * @param  tileDimensions The dimensions of a (de)compressed tile.
352      *
353      * @return                Object array row.
354      *
355      * @throws FitsException  If the row doesn't exist, or cannot be read.
356      */
357     Object[] getRow(final int[] positions, final int[] tileDimensions) throws FitsException {
358         final int[] tileIndexes = getTileIndexes(ArrayFuncs.reverseIndices(positions),
359                 ArrayFuncs.reverseIndices(tileDimensions));
360         final int rowNumber = getRowNumber(tileIndexes);
361         return compressedImageHDU.getRow(rowNumber);
362     }
363 
364     int getRowNumber(final int[] tileIndexes) throws FitsException {
365         int offset = 0;
366         final int[] tableDimensions = getTableDimensions();
367         for (int i = 0; i < tableDimensions.length; i++) {
368             if (i > 0) {
369                 offset += tileIndexes[i] * tableDimensions[i - 1];
370             } else {
371                 offset += tileIndexes[i];
372             }
373         }
374         return offset;
375     }
376 
377     /**
378      * Obtain the starting corner within the starting tile.
379      *
380      * @param  corners The pixel corners specified.
381      *
382      * @return         Multidimensional array of pixel corners
383      */
384     int[] getTileOffsets(final int[] corners, final int[] tileDimensions) {
385         final int numberOfDimensions = getNumberOfDimensions();
386         final int[] tileOffsets = new int[numberOfDimensions];
387 
388         for (int i = 0; i < numberOfDimensions; i++) {
389             final int tileDimension = tileDimensions[i];
390             final int pixelOffset = corners[i];
391             if (pixelOffset % tileDimension == 0 || tileDimension == 1) {
392                 tileOffsets[i] = 0;
393             } else {
394                 final int currentTile = corners[i] / tileDimensions[i];
395                 final int lastTile = Math.max(0, currentTile - 1);
396 
397                 // Account for the zeroth index by adding another tile dimension at the end.
398                 final int pixelsToEndOfLastTile = (lastTile * tileDimension) + tileDimension;
399                 final int tileOffset;
400                 if (pixelOffset < tileDimension) {
401                     tileOffset = pixelOffset;
402                 } else {
403                     tileOffset = pixelOffset - pixelsToEndOfLastTile;
404                 }
405 
406                 tileOffsets[i] = tileOffset;
407             }
408         }
409 
410         return tileOffsets;
411     }
412 
413     @Override
414     public Object getCompleteImage() throws IOException {
415         try {
416             return compressedImageHDU.asImageHDU().getData().getData();
417         } catch (FitsException fitsException) {
418             throw new IOException(fitsException.getMessage(), fitsException);
419         }
420     }
421 
422     @Override
423     public void getTile(Object output, int[] corners, int[] lengths) throws IOException {
424         final int[] steps = new int[lengths.length];
425         Arrays.fill(steps, 1);
426         getTile(output, corners, lengths, steps);
427     }
428 
429     @Override
430     public void getTile(Object output, int[] corners, int[] lengths, int[] steps) throws IOException {
431         final int[] imageDimensions = getImageDimensions();
432 
433         if (corners.length != imageDimensions.length || lengths.length != imageDimensions.length
434                 || steps.length != imageDimensions.length) {
435             throw new IOException("Inconsistent sub-image request");
436         }
437         if (output == null) {
438             throw new IOException("Attempt to write to null data output");
439         }
440         for (int i = 0; i < imageDimensions.length; i++) {
441             if (corners[i] < 0 || lengths[i] < 0 || corners[i] + lengths[i] > imageDimensions[i]) {
442                 throw new IOException("Sub-image not within image");
443             }
444         }
445 
446         if (!(output instanceof ArrayDataOutput)) {
447             throw new UnsupportedOperationException("Only streaming to ArrayDataOutput is supported.  "
448                     + "See getTile(ArrayDataOutput, int[], int[], int[].");
449         }
450         try {
451             getTile((ArrayDataOutput) output, imageDimensions, corners, lengths, steps);
452         } catch (FitsException fitsException) {
453             throw new IOException(fitsException.getMessage(), fitsException);
454         }
455     }
456 
457     @Override
458     public Object getTile(int[] corners, int[] lengths) throws IOException {
459         throw new UnsupportedOperationException(
460                 "Only streaming to ArrayDataOutput is supported.  " + "See getTile(ArrayDataOutput, int[], int[], int[].");
461     }
462 
463     void initRowOption(final ICompressOption option, final Object[] row) {
464         final int zScaleColumnIndex = columnNames.indexOf(Compression.ZSCALE_COLUMN);
465         final int zZeroColumnIndex = columnNames.indexOf(Compression.ZZERO_COLUMN);
466         if (option instanceof QuantizeOption) {
467             final double bScale = zScaleColumnIndex >= 0 ? ((double[]) row[zScaleColumnIndex])[0] : Double.NaN;
468             ((QuantizeOption) option).setBScale(bScale);
469 
470             final double bZero = zZeroColumnIndex >= 0 ? ((double[]) row[zZeroColumnIndex])[0] : Double.NaN;
471             ((QuantizeOption) option).setBZero(bZero);
472         }
473     }
474 
475     Header getHeader() {
476         return compressedImageHDU.getHeader();
477     }
478 
479     private String getQuantizAlgorithmName() {
480         return getHeader().getStringValue(Compression.ZQUANTIZ);
481     }
482 
483     private String getCompressionAlgorithmName() {
484         return getHeader().getStringValue(Compression.ZCMPTYPE);
485     }
486 
487     int getBlockSize() {
488         final int axesLength = getNumberOfDimensions();
489         for (int i = 0; i < axesLength; i++) {
490             final int nextAxis = i + 1;
491             final String zNameValue = getHeader().getStringValue(Compression.ZNAMEn.n(nextAxis));
492             if (Compression.BLOCKSIZE.equals(zNameValue)) {
493                 return getHeader().getIntValue(Compression.ZVALn.n(nextAxis));
494             }
495         }
496 
497         return DEFAULT_BLOCK_SIZE;
498     }
499 
500     /**
501      * Obtain the dimension count of this image (ZNAXIS). Tests can override.
502      *
503      * @return integer of dimension count. Never null.
504      */
505     int getNumberOfDimensions() {
506         return getHeader().getIntValue(Compression.ZNAXIS);
507     }
508 
509     int getZBitPix() {
510         return getHeader().getIntValue(Compression.ZBITPIX);
511     }
512 
513     int getImageAxisLength(final int axis) {
514         return getHeader().getIntValue(Compression.ZNAXISn.n(axis));
515     }
516 
517     int[] getTableDimensions() throws FitsException {
518         final int n = getNumberOfDimensions();
519         final int[] tableDimensions = new int[n];
520         for (int i = 0; i < n; i++) {
521             tableDimensions[i] = Double
522                     .valueOf(Math
523                             .ceil(Integer.valueOf(getImageAxisLength(i + 1)).doubleValue() / getTileDimensionLength(i + 1)))
524                     .intValue();
525         }
526 
527         return tableDimensions;
528     }
529 
530     /**
531      * Obtain the full image dimensions of the image that is represented by this compressed binary table.
532      *
533      * @return The image dimensions.
534      */
535     int[] getImageDimensions() {
536         final int n = getNumberOfDimensions();
537         final int[] imageDimensions = new int[n];
538         final Header header = getHeader();
539         for (int i = 0; i < n; i++) {
540             imageDimensions[n - i - 1] = header.getIntValue(Compression.ZNAXISn.n(i + 1));
541         }
542 
543         return imageDimensions;
544     }
545 
546     int getTileHeight() {
547         return getHeader().getIntValue(Compression.ZTILEn.n(2), 1);
548     }
549 
550     int getTileWidth() {
551         return getHeader().getIntValue(Compression.ZTILEn.n(1), getHeader().getIntValue(Compression.ZNAXISn.n(1)));
552     }
553 
554     int[] getTileDimensions() throws FitsException {
555         final int totalDimensions = getNumberOfDimensions();
556         final int[] tileDimensions = new int[totalDimensions];
557         for (int n = 0; n < totalDimensions; n++) {
558             tileDimensions[n] = getTileDimensionLength(n + 1);
559         }
560 
561         return tileDimensions;
562     }
563 
564     int getTileDimensionLength(final int dimension) throws FitsException {
565         final int totalDimensions = getNumberOfDimensions();
566 
567         if (dimension < 1) {
568             throw new FitsException("Dimensions are 1-based (got " + dimension + ").");
569         }
570         if (dimension > totalDimensions) {
571             throw new FitsException("Trying to get tile for dimension " + dimension + " where there are only "
572                     + totalDimensions + " dimensions in total.");
573         }
574 
575         final int dimensionLength;
576         if (dimension == 1) {
577             dimensionLength = getHeader().getIntValue(Compression.ZTILEn.n(1),
578                     getHeader().getIntValue(Compression.ZNAXISn.n(1)));
579         } else {
580             dimensionLength = getHeader().getIntValue(Compression.ZTILEn.n(dimension), 1);
581         }
582 
583         return dimensionLength;
584     }
585 
586     int getTileSize() {
587         final int n = getNumberOfDimensions();
588         int tileSize = getHeader().getIntValue(Compression.ZTILEn.n(1), getHeader().getIntValue(Compression.ZNAXISn.n(1)));
589         for (int i = 2; i <= n; i++) {
590             tileSize *= getHeader().getIntValue(Compression.ZTILEn.n(i), 1);
591         }
592 
593         return tileSize;
594     }
595 }