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         compressAlgorithm = algo;
307 
308         if (algo.equals(Compression.ZCMPTYPE_RICE_ONE) && FitsFactory.isAllowHeaderRepairs()) {
309             compressAlgorithm = Compression.ZCMPTYPE_RICE_1;
310             Logger.getLogger(HeaderCard.class.getName()).warning("Repaired non-standard ZCMPTYPE value: "
311                     + Compression.ZCMPTYPE_RICE_ONE + " to " + Compression.ZCMPTYPE_RICE_1);
312             return this;
313         }
314 
315         if (algo.equals(Compression.ZCMPTYPE_GZIP_1) || algo.equals(Compression.ZCMPTYPE_GZIP_2)
316                 || algo.equals(Compression.ZCMPTYPE_RICE_1) || algo.equals(Compression.ZCMPTYPE_PLIO_1)
317                 || algo.equals(Compression.ZCMPTYPE_HCOMPRESS_1) || algo.equals(Compression.ZCMPTYPE_NOCOMPRESS)) {
318             return this;
319         }
320 
321         throw new FitsException("Invalid ZCMPTYPE value: " + algo);
322     }
323 
324     /**
325      * Sets the quantization algorithm, via a <code>ZQUANTIZ</code> header card or equivalent. The card must contain one
326      * of the values recognized by the FITS standard. If not, <code>null</code> will be set instead.
327      *
328      * @param  quantAlgorithmCard The header card that specifies the compression algorithm, with one of the standard
329      *                                recognized values such as {@link Compression#ZQUANTIZ_NO_DITHER}, or
330      *                                <code>null</code>.
331      *
332      * @return                    itself
333      *
334      * @see                       #getQuantAlgorithm()
335      * @see                       #setCompressAlgorithm(HeaderCard)
336      */
337     public synchronized TiledImageCompressionOperation setQuantAlgorithm(HeaderCard quantAlgorithmCard) {
338         quantAlgorithm = null;
339 
340         if (quantAlgorithmCard == null) {
341             return this;
342         }
343 
344         String algo = quantAlgorithmCard.getValue().toUpperCase();
345 
346         if (algo.equals(Compression.ZQUANTIZ_NO_DITHER) || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1)
347                 || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_2)) {
348             quantAlgorithm = algo;
349         } else {
350             Logger.getLogger(HeaderCard.class.getName()).warning("Ignored invalid ZQUANTIZ value: " + algo);
351         }
352 
353         return this;
354     }
355 
356     /**
357      * Returns the name of the currently configured quantization algorithm.
358      *
359      * @return The name of the standard quantization algorithm (i.e. a FITS standard value for the <code>ZQUANTIZ</code>
360      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
361      *             value was set before.
362      *
363      * @see    #setQuantAlgorithm(HeaderCard)
364      * @see    #getCompressAlgorithm()
365      *
366      * @since  1.18
367      */
368     public synchronized String getQuantAlgorithm() {
369         return quantAlgorithm;
370     }
371 
372     /**
373      * Returns the name of the currently configured compression algorithm.
374      *
375      * @return The name of the standard compression algorithm (i.e. a FITS standard value for the <code>ZCMPTYPE</code>
376      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
377      *             value was set before.
378      *
379      * @see    #setCompressAlgorithm(HeaderCard)
380      * @see    #getQuantAlgorithm()
381      *
382      * @since  1.18
383      */
384     public String getCompressAlgorithm() {
385         return compressAlgorithm;
386     }
387 
388     private <T> T getNullableColumn(Header header, Class<T> class1, String columnName) throws FitsException {
389         for (int i = 1; i <= binaryTable.getNCols(); i++) {
390             String val = header.getStringValue(TTYPEn.n(i));
391             if (val != null && val.trim().equals(columnName)) {
392                 return class1.cast(binaryTable.getColumn(i - 1));
393             }
394         }
395         return null;
396     }
397 
398     private void processAllTiles() {
399         compressOptions();
400         ExecutorService threadPool = FitsFactory.threadPool();
401         for (TileCompressionOperation tileOperation : getTileOperations()) {
402             tileOperation.execute(threadPool);
403         }
404         for (TileCompressionOperation tileOperation : getTileOperations()) {
405             tileOperation.waitForResult();
406         }
407     }
408 
409     private void readAxis(Header header) throws FitsException {
410         if (hasAxes()) {
411             return;
412         }
413         int naxis = header.getIntValue(ZNAXIS);
414         int[] axes = new int[naxis];
415         for (int i = 1; i <= naxis; i++) {
416             int axisValue = header.getIntValue(ZNAXISn.n(i), -1);
417             if (axisValue == -1) {
418                 throw new FitsException("Required ZNAXISn not found");
419             }
420             axes[naxis - i] = axisValue;
421         }
422         setAxes(axes);
423     }
424 
425     private void readBaseType(Header header) {
426         if (getBaseType() == null) {
427             int zBitPix = header.getIntValue(ZBITPIX);
428             ElementType<Buffer> elementType = ElementType.forNearestBitpix(zBitPix);
429             if (elementType == ElementType.UNKNOWN) {
430                 throw new IllegalArgumentException("Illegal value for ZBITPIX: " + zBitPix);
431             }
432             setBaseType(elementType);
433         }
434     }
435 
436     private void readCompressionHeaders(Header header) {
437         compressOptions().getCompressionParameters().getValuesFromHeader(new HeaderAccess(header));
438     }
439 
440     private void readTileAxis(Header header) throws FitsException {
441         if (hasTileAxes()) {
442             return;
443         }
444 
445         int naxes = getNAxes();
446         int[] tileAxes = new int[naxes];
447         // TODO
448         // The FITS default is to tile by row (Pence, W., et al. 2000, ASPC, 216, 551)
449         // However, this library defaulted to full image size by default...
450         for (int i = 1; i <= naxes; i++) {
451             tileAxes[naxes - i] = header.getIntValue(ZTILEn.n(i), i == 1 ? header.getIntValue(ZNAXISn.n(1)) : 1);
452         }
453 
454         setTileAxes(tileAxes);
455     }
456 
457     private <T> Object setInColumn(Object column, boolean predicate, TileCompressionOperation tileOperation, Class<T> clazz,
458             T value) {
459         if (predicate) {
460             if (column == null) {
461                 column = Array.newInstance(clazz, getNumberOfTileOperations());
462             }
463             Array.set(column, tileOperation.getTileIndex(), value);
464         }
465         return column;
466     }
467 
468     private synchronized void writeColumns(BinaryTableHDU hdu) throws FitsException {
469         Object compressedColumn = null;
470         Object uncompressedColumn = null;
471         Object gzipColumn = null;
472         for (TileCompressionOperation tileOperation : getTileOperations()) {
473             TileCompressionType compression = tileOperation.getCompressionType();
474             byte[] compressedData = tileOperation.getCompressedData();
475 
476             compressedColumn = setInColumn(compressedColumn, compression == COMPRESSED, tileOperation, byte[].class,
477                     compressedData);
478             gzipColumn = setInColumn(gzipColumn, compression == GZIP_COMPRESSED, tileOperation, byte[].class,
479                     compressedData);
480             uncompressedColumn = setInColumn(uncompressedColumn, compression == UNCOMPRESSED, tileOperation, byte[].class,
481                     compressedData);
482         }
483         setNullEntries(compressedColumn, new byte[0]);
484         setNullEntries(gzipColumn, new byte[0]);
485         setNullEntries(uncompressedColumn, new byte[0]);
486         addColumnToTable(hdu, compressedColumn, COMPRESSED_DATA_COLUMN);
487         addColumnToTable(hdu, gzipColumn, GZIP_COMPRESSED_DATA_COLUMN);
488         addColumnToTable(hdu, uncompressedColumn, UNCOMPRESSED_DATA_COLUMN);
489         if (imageNullPixelMask != null) {
490             addColumnToTable(hdu, imageNullPixelMask.getColumn(), NULL_PIXEL_MASK_COLUMN);
491         }
492         compressOptions.getCompressionParameters().addColumnsToTable(hdu);
493         hdu.getData().fillHeader(hdu.getHeader());
494     }
495 
496     private void writeHeader(Header header) throws FitsException {
497         HeaderCardBuilder cardBuilder = header//
498                 .card(ZBITPIX).value(getBaseType().bitPix())//
499                 .card(ZCMPTYPE).value(compressAlgorithm);
500         int[] tileAxes = getTileAxes();
501         int naxes = tileAxes.length;
502         for (int i = 1; i <= naxes; i++) {
503             cardBuilder.card(ZTILEn.n(i)).value(tileAxes[naxes - i]);
504         }
505         compressOptions().getCompressionParameters().setValuesInHeader(new HeaderAccess(header));
506         if (imageNullPixelMask != null) {
507             cardBuilder.card(ZMASKCMP).value(imageNullPixelMask.getCompressAlgorithm());
508         }
509     }
510 
511     protected BinaryTable getBinaryTable() {
512         return binaryTable;
513     }
514 
515     protected ImageNullPixelMask getImageNullPixelMask() {
516         return imageNullPixelMask;
517     }
518 
519 }