View Javadoc
1   package nom.tam.image.compression.tile;
2   
3   import java.lang.reflect.Array;
4   import java.nio.Buffer;
5   import java.nio.ByteBuffer;
6   import java.util.Locale;
7   import java.util.concurrent.ExecutorService;
8   import java.util.logging.Logger;
9   
10  import nom.tam.fits.BinaryTable;
11  import nom.tam.fits.BinaryTableHDU;
12  import nom.tam.fits.FitsException;
13  import nom.tam.fits.FitsFactory;
14  import nom.tam.fits.Header;
15  import nom.tam.fits.HeaderCard;
16  import nom.tam.fits.HeaderCardBuilder;
17  import nom.tam.fits.compression.algorithm.api.ICompressOption;
18  import nom.tam.fits.compression.algorithm.api.ICompressorControl;
19  import nom.tam.fits.compression.provider.CompressorProvider;
20  import nom.tam.fits.compression.provider.param.api.HeaderAccess;
21  import nom.tam.fits.header.Compression;
22  import nom.tam.image.compression.tile.mask.ImageNullPixelMask;
23  import nom.tam.image.tile.operation.AbstractTiledImageOperation;
24  import nom.tam.image.tile.operation.TileArea;
25  import nom.tam.util.type.ElementType;
26  
27  /*
28   * #%L
29   * nom.tam FITS library
30   * %%
31   * Copyright (C) 1996 - 2024 nom-tam-fits
32   * %%
33   * This is free and unencumbered software released into the public domain.
34   *
35   * Anyone is free to copy, modify, publish, use, compile, sell, or
36   * distribute this software, either in source code form or as a compiled
37   * binary, for any purpose, commercial or non-commercial, and by any
38   * means.
39   *
40   * In jurisdictions that recognize copyright laws, the author or authors
41   * of this software dedicate any and all copyright interest in the
42   * software to the public domain. We make this dedication for the benefit
43   * of the public at large and to the detriment of our heirs and
44   * successors. We intend this dedication to be an overt act of
45   * relinquishment in perpetuity of all present and future rights to this
46   * software under copyright law.
47   *
48   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
49   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
52   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
53   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
54   * OTHER DEALINGS IN THE SOFTWARE.
55   * #L%
56   */
57  
58  import static nom.tam.fits.header.Compression.COMPRESSED_DATA_COLUMN;
59  import static nom.tam.fits.header.Compression.GZIP_COMPRESSED_DATA_COLUMN;
60  import static nom.tam.fits.header.Compression.NULL_PIXEL_MASK_COLUMN;
61  import static nom.tam.fits.header.Compression.UNCOMPRESSED_DATA_COLUMN;
62  import static nom.tam.fits.header.Compression.ZBITPIX;
63  import static nom.tam.fits.header.Compression.ZCMPTYPE;
64  import static nom.tam.fits.header.Compression.ZCMPTYPE_GZIP_1;
65  import static nom.tam.fits.header.Compression.ZMASKCMP;
66  import static nom.tam.fits.header.Compression.ZNAXIS;
67  import static nom.tam.fits.header.Compression.ZNAXISn;
68  import static nom.tam.fits.header.Compression.ZQUANTIZ;
69  import static nom.tam.fits.header.Compression.ZSCALE_COLUMN;
70  import static nom.tam.fits.header.Compression.ZTILEn;
71  import static nom.tam.fits.header.Compression.ZZERO_COLUMN;
72  import static nom.tam.fits.header.Standard.TFIELDS;
73  import static nom.tam.fits.header.Standard.TTYPEn;
74  import static nom.tam.image.compression.tile.TileCompressionType.COMPRESSED;
75  import static nom.tam.image.compression.tile.TileCompressionType.GZIP_COMPRESSED;
76  import static nom.tam.image.compression.tile.TileCompressionType.UNCOMPRESSED;
77  
78  /**
79   * (<i>for internal use</i>) Compresseses an entire image, by parallel processing image tiles. This class represents a
80   * complete compression of a tiled describing an image ordered from left to right and top down. the tiles all have the
81   * same geometry only the tiles at the right and bottom sides can have different (truncated) sizes.
82   */
83  @SuppressWarnings("deprecation")
84  public class TiledImageCompressionOperation extends AbstractTiledImageOperation<TileCompressionOperation> {
85  
86      /**
87       * ZCMPTYPE name of the algorithm that was used to compress
88       */
89      private String compressAlgorithm;
90  
91      private final BinaryTable binaryTable;
92  
93      private ByteBuffer compressedWholeArea;
94  
95      // Note: field is initialized lazily: use getter within class!
96      private ICompressorControl compressorControl;
97  
98      // Note: field is initialized lazily: use compressOptions() within class!
99      private ICompressOption compressOptions;
100 
101     /**
102      * ZQUANTIZ name of the algorithm that was used to quantize
103      */
104     private String quantAlgorithm;
105 
106     // Note: field is initialized lazily: use getter within class!
107     private ICompressorControl gzipCompressorControl;
108 
109     private ImageNullPixelMask imageNullPixelMask;
110 
111     private static void addColumnToTable(BinaryTableHDU hdu, Object column, String columnName) throws FitsException {
112         if (column != null) {
113             hdu.setColumnName(hdu.addColumn(column) - 1, columnName, null);
114         }
115     }
116 
117     private static void setNullEntries(Object column, Object defaultValue) {
118         if (column != null) {
119             for (int index = 0; index < Array.getLength(column); index++) {
120                 if (Array.get(column, index) == null) {
121                     Array.set(column, index, defaultValue);
122                 }
123             }
124         }
125     }
126 
127     /**
128      * create a TiledImageCompressionOperation based on a compressed image data.
129      *
130      * @param binaryTable the compressed image data.
131      */
132     public TiledImageCompressionOperation(BinaryTable binaryTable) {
133         super(TileCompressionOperation.class);
134         this.binaryTable = binaryTable;
135     }
136 
137     public void compress(BinaryTableHDU hdu) throws FitsException {
138         processAllTiles();
139         writeColumns(hdu);
140         writeHeader(hdu.getHeader());
141     }
142 
143     @Override
144     public synchronized ICompressOption compressOptions() {
145         if (compressorControl == null) {
146             getCompressorControl();
147             compressOptions = compressorControl.option();
148             if (quantAlgorithm != null) {
149                 Header h = new Header();
150                 h.addLine(HeaderCard.create(ZQUANTIZ, quantAlgorithm));
151                 compressOptions.getCompressionParameters().getValuesFromHeader(h);
152             }
153             compressOptions.getCompressionParameters().initializeColumns(getNumberOfTileOperations());
154         }
155         return compressOptions;
156     }
157 
158     public Buffer decompress() {
159         Buffer decompressedWholeArea = getBaseType().newBuffer(getBufferSize());
160         for (TileCompressionOperation tileOperation : getTileOperations()) {
161             tileOperation.setWholeImageBuffer(decompressedWholeArea);
162         }
163         processAllTiles();
164         decompressedWholeArea.rewind();
165         return decompressedWholeArea;
166     }
167 
168     public void forceNoLoss(int x, int y, int width, int heigth) {
169         TileArea tileArea = new TileArea().start(x, y).end(x + width, y + heigth);
170         for (TileCompressionOperation operation : getTileOperations()) {
171             if (operation.getArea().intersects(tileArea)) {
172                 operation.forceNoLoss(true);
173             }
174         }
175     }
176 
177     @Override
178     public ByteBuffer getCompressedWholeArea() {
179         return compressedWholeArea;
180     }
181 
182     @Override
183     public synchronized ICompressorControl getCompressorControl() {
184         if (compressorControl == null) {
185             compressorControl = CompressorProvider.findCompressorControl(quantAlgorithm, compressAlgorithm,
186                     getBaseType().primitiveClass());
187             if (compressorControl == null) {
188                 throw new IllegalStateException(
189                         "Found no compressor control for compression algorithm:" + compressAlgorithm + //
190                                 " (quantize algorithm = " + quantAlgorithm + ", base type = "
191                                 + getBaseType().primitiveClass() + ")");
192             }
193         }
194         return compressorControl;
195     }
196 
197     @Override
198     public synchronized ICompressorControl getGzipCompressorControl() {
199         if (gzipCompressorControl == null) {
200             gzipCompressorControl = CompressorProvider.findCompressorControl(null, ZCMPTYPE_GZIP_1,
201                     getBaseType().primitiveClass());
202         }
203         return gzipCompressorControl;
204     }
205 
206     public TiledImageCompressionOperation prepareUncompressedData(final Buffer buffer) throws FitsException {
207         compressedWholeArea = ByteBuffer.wrap(new byte[getBaseType().size() * getBufferSize()]);
208         createTiles(new TileCompressorInitialisation(this, buffer));
209         compressedWholeArea.rewind();
210         return this;
211     }
212 
213     /**
214      * preserve null values, where the value representing null is specified as a parameter. This parameter is ignored
215      * for floating point values where NaN is used as null value.
216      *
217      * @param  nullValue            the value representing null for byte/short and integer pixel values
218      * @param  compressionAlgorithm compression algorithm to use for the null pixel mask
219      *
220      * @return                      the created null pixel mask
221      */
222     public ImageNullPixelMask preserveNulls(long nullValue, String compressionAlgorithm) {
223         imageNullPixelMask = new ImageNullPixelMask(getTileOperations().length, nullValue, compressionAlgorithm);
224         for (TileCompressionOperation tileOperation : getTileOperations()) {
225             tileOperation.createImageNullPixelMask(getImageNullPixelMask());
226         }
227         return imageNullPixelMask;
228     }
229 
230     private synchronized void setQuantAlgorithm(final Header header) {
231         setQuantAlgorithm(header.getCard(ZQUANTIZ));
232 
233         if (quantAlgorithm != null) {
234             return;
235         }
236 
237         // AK: If no ZQUANTIZ keyword, but has ZSCALE and ZZERO columns, then use NO_DITHER quantiz...
238         boolean hasScale = false;
239         boolean hasZero = false;
240 
241         int nFields = header.getIntValue(TFIELDS);
242 
243         for (int i = 1; i <= nFields; i++) {
244             String type = header.getStringValue(TTYPEn.n(i));
245 
246             if (ZSCALE_COLUMN.equals(type)) {
247                 hasScale = true;
248             } else if (ZZERO_COLUMN.equals(type)) {
249                 hasZero = true;
250             } else {
251                 continue;
252             }
253 
254             if (hasScale && hasZero) {
255                 setQuantAlgorithm(HeaderCard.create(ZQUANTIZ, Compression.ZQUANTIZ_NO_DITHER));
256                 break;
257             }
258         }
259     }
260 
261     public TiledImageCompressionOperation read(final Header header) throws FitsException {
262         readPrimaryHeaders(header);
263         setCompressAlgorithm(header.getCard(ZCMPTYPE));
264         setQuantAlgorithm(header);
265 
266         createTiles(new TileDecompressorInitialisation(this, //
267                 getNullableColumn(header, Object[].class, UNCOMPRESSED_DATA_COLUMN), //
268                 getNullableColumn(header, Object[].class, COMPRESSED_DATA_COLUMN), //
269                 getNullableColumn(header, Object[].class, GZIP_COMPRESSED_DATA_COLUMN), //
270                 header));
271         byte[][] nullPixels = getNullableColumn(header, byte[][].class, NULL_PIXEL_MASK_COLUMN);
272         if (nullPixels != null) {
273             preserveNulls(0L, header.getStringValue(ZMASKCMP)).setColumn(nullPixels);
274         }
275         readCompressionHeaders(header);
276         return this;
277     }
278 
279     public void readPrimaryHeaders(Header header) throws FitsException {
280         readBaseType(header);
281         readAxis(header);
282         readTileAxis(header);
283     }
284 
285     /**
286      * Sets the compression algorithm, via a <code>ZCMPTYPE</code> header card or equivalent. The card must contain one
287      * of the values recognized by the FITS standard. If not, <code>null</code> will be set instead.
288      *
289      * @param  compressAlgorithmCard The header card that specifies the compression algorithm, with one of the standard
290      *                                   recognized values such as {@link Compression#ZCMPTYPE_GZIP_1}, or
291      *                                   <code>null</code>.
292      *
293      * @return                       itself
294      *
295      * @see                          #getCompressAlgorithm()
296      * @see                          #setQuantAlgorithm(HeaderCard)
297      */
298     public TiledImageCompressionOperation setCompressAlgorithm(HeaderCard compressAlgorithmCard) {
299         compressAlgorithm = null;
300 
301         if (compressAlgorithmCard == null) {
302             return this;
303         }
304 
305         String algo = compressAlgorithmCard.getValue().toUpperCase(Locale.US);
306         if (algo.equals(Compression.ZCMPTYPE_GZIP_1) || algo.equals(Compression.ZCMPTYPE_GZIP_2)
307                 || algo.equals(Compression.ZCMPTYPE_RICE_1) || algo.equals(Compression.ZCMPTYPE_PLIO_1)
308                 || algo.equals(Compression.ZCMPTYPE_HCOMPRESS_1) || algo.equals(Compression.ZCMPTYPE_NOCOMPRESS)) {
309             compressAlgorithm = algo;
310         } else {
311             Logger.getLogger(HeaderCard.class.getName()).warning("Ignored invalid ZCMPTYPE value: " + algo);
312         }
313 
314         return this;
315     }
316 
317     /**
318      * Sets the quantization algorithm, via a <code>ZQUANTIZ</code> header card or equivalent. The card must contain one
319      * of the values recognized by the FITS standard. If not, <code>null</code> will be set instead.
320      *
321      * @param  quantAlgorithmCard The header card that specifies the compression algorithm, with one of the standard
322      *                                recognized values such as {@link Compression#ZQUANTIZ_NO_DITHER}, or
323      *                                <code>null</code>.
324      *
325      * @return                    itself
326      *
327      * @see                       #getQuantAlgorithm()
328      * @see                       #setCompressAlgorithm(HeaderCard)
329      */
330     public synchronized TiledImageCompressionOperation setQuantAlgorithm(HeaderCard quantAlgorithmCard) {
331         quantAlgorithm = null;
332 
333         if (quantAlgorithmCard == null) {
334             return this;
335         }
336 
337         String algo = quantAlgorithmCard.getValue().toUpperCase();
338 
339         if (algo.equals(Compression.ZQUANTIZ_NO_DITHER) || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1)
340                 || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_2)) {
341             quantAlgorithm = algo;
342         } else {
343             Logger.getLogger(HeaderCard.class.getName()).warning("Ignored invalid ZQUANTIZ value: " + algo);
344         }
345 
346         return this;
347     }
348 
349     /**
350      * Returns the name of the currently configured quantization algorithm.
351      *
352      * @return The name of the standard quantization algorithm (i.e. a FITS standard value for the <code>ZQUANTIZ</code>
353      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
354      *             value was set before.
355      *
356      * @see    #setQuantAlgorithm(HeaderCard)
357      * @see    #getCompressAlgorithm()
358      *
359      * @since  1.18
360      */
361     public synchronized String getQuantAlgorithm() {
362         return quantAlgorithm;
363     }
364 
365     /**
366      * Returns the name of the currently configured compression algorithm.
367      *
368      * @return The name of the standard compression algorithm (i.e. a FITS standard value for the <code>ZCMPTYPE</code>
369      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
370      *             value was set before.
371      *
372      * @see    #setCompressAlgorithm(HeaderCard)
373      * @see    #getQuantAlgorithm()
374      *
375      * @since  1.18
376      */
377     public String getCompressAlgorithm() {
378         return compressAlgorithm;
379     }
380 
381     private <T> T getNullableColumn(Header header, Class<T> class1, String columnName) throws FitsException {
382         for (int i = 1; i <= binaryTable.getNCols(); i++) {
383             String val = header.getStringValue(TTYPEn.n(i));
384             if (val != null && val.trim().equals(columnName)) {
385                 return class1.cast(binaryTable.getColumn(i - 1));
386             }
387         }
388         return null;
389     }
390 
391     private void processAllTiles() {
392         compressOptions();
393         ExecutorService threadPool = FitsFactory.threadPool();
394         for (TileCompressionOperation tileOperation : getTileOperations()) {
395             tileOperation.execute(threadPool);
396         }
397         for (TileCompressionOperation tileOperation : getTileOperations()) {
398             tileOperation.waitForResult();
399         }
400     }
401 
402     private void readAxis(Header header) throws FitsException {
403         if (hasAxes()) {
404             return;
405         }
406         int naxis = header.getIntValue(ZNAXIS);
407         int[] axes = new int[naxis];
408         for (int i = 1; i <= naxis; i++) {
409             int axisValue = header.getIntValue(ZNAXISn.n(i), -1);
410             if (axisValue == -1) {
411                 throw new FitsException("Required ZNAXISn not found");
412             }
413             axes[naxis - i] = axisValue;
414         }
415         setAxes(axes);
416     }
417 
418     private void readBaseType(Header header) {
419         if (getBaseType() == null) {
420             int zBitPix = header.getIntValue(ZBITPIX);
421             ElementType<Buffer> elementType = ElementType.forNearestBitpix(zBitPix);
422             if (elementType == ElementType.UNKNOWN) {
423                 throw new IllegalArgumentException("Illegal value for ZBITPIX: " + zBitPix);
424             }
425             setBaseType(elementType);
426         }
427     }
428 
429     private void readCompressionHeaders(Header header) {
430         compressOptions().getCompressionParameters().getValuesFromHeader(new HeaderAccess(header));
431     }
432 
433     private void readTileAxis(Header header) throws FitsException {
434         if (hasTileAxes()) {
435             return;
436         }
437 
438         int naxes = getNAxes();
439         int[] tileAxes = new int[naxes];
440         // TODO
441         // The FITS default is to tile by row (Pence, W., et al. 2000, ASPC, 216, 551)
442         // However, this library defaulted to full image size by default...
443         for (int i = 1; i <= naxes; i++) {
444             tileAxes[naxes - i] = header.getIntValue(ZTILEn.n(i), i == 1 ? header.getIntValue(ZNAXISn.n(1)) : 1);
445         }
446 
447         setTileAxes(tileAxes);
448     }
449 
450     private <T> Object setInColumn(Object column, boolean predicate, TileCompressionOperation tileOperation, Class<T> clazz,
451             T value) {
452         if (predicate) {
453             if (column == null) {
454                 column = Array.newInstance(clazz, getNumberOfTileOperations());
455             }
456             Array.set(column, tileOperation.getTileIndex(), value);
457         }
458         return column;
459     }
460 
461     private synchronized void writeColumns(BinaryTableHDU hdu) throws FitsException {
462         Object compressedColumn = null;
463         Object uncompressedColumn = null;
464         Object gzipColumn = null;
465         for (TileCompressionOperation tileOperation : getTileOperations()) {
466             TileCompressionType compression = tileOperation.getCompressionType();
467             byte[] compressedData = tileOperation.getCompressedData();
468 
469             compressedColumn = setInColumn(compressedColumn, compression == COMPRESSED, tileOperation, byte[].class,
470                     compressedData);
471             gzipColumn = setInColumn(gzipColumn, compression == GZIP_COMPRESSED, tileOperation, byte[].class,
472                     compressedData);
473             uncompressedColumn = setInColumn(uncompressedColumn, compression == UNCOMPRESSED, tileOperation, byte[].class,
474                     compressedData);
475         }
476         setNullEntries(compressedColumn, new byte[0]);
477         setNullEntries(gzipColumn, new byte[0]);
478         setNullEntries(uncompressedColumn, new byte[0]);
479         addColumnToTable(hdu, compressedColumn, COMPRESSED_DATA_COLUMN);
480         addColumnToTable(hdu, gzipColumn, GZIP_COMPRESSED_DATA_COLUMN);
481         addColumnToTable(hdu, uncompressedColumn, UNCOMPRESSED_DATA_COLUMN);
482         if (imageNullPixelMask != null) {
483             addColumnToTable(hdu, imageNullPixelMask.getColumn(), NULL_PIXEL_MASK_COLUMN);
484         }
485         compressOptions.getCompressionParameters().addColumnsToTable(hdu);
486         hdu.getData().fillHeader(hdu.getHeader());
487     }
488 
489     private void writeHeader(Header header) throws FitsException {
490         HeaderCardBuilder cardBuilder = header//
491                 .card(ZBITPIX).value(getBaseType().bitPix())//
492                 .card(ZCMPTYPE).value(compressAlgorithm);
493         int[] tileAxes = getTileAxes();
494         int naxes = tileAxes.length;
495         for (int i = 1; i <= naxes; i++) {
496             cardBuilder.card(ZTILEn.n(i)).value(tileAxes[naxes - i]);
497         }
498         compressOptions().getCompressionParameters().setValuesInHeader(new HeaderAccess(header));
499         if (imageNullPixelMask != null) {
500             cardBuilder.card(ZMASKCMP).value(imageNullPixelMask.getCompressAlgorithm());
501         }
502     }
503 
504     protected BinaryTable getBinaryTable() {
505         return binaryTable;
506     }
507 
508     protected ImageNullPixelMask getImageNullPixelMask() {
509         return imageNullPixelMask;
510     }
511 
512 }