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({"javadoc", "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     /** For thread synchronization */
112     private Object lock = new Object();
113 
114     private static void addColumnToTable(BinaryTableHDU hdu, Object column, String columnName) throws FitsException {
115         if (column != null) {
116             hdu.setColumnName(hdu.addColumn(column) - 1, columnName, null);
117         }
118     }
119 
120     private static void setNullEntries(Object column, Object defaultValue) {
121         if (column != null) {
122             for (int index = 0; index < Array.getLength(column); index++) {
123                 if (Array.get(column, index) == null) {
124                     Array.set(column, index, defaultValue);
125                 }
126             }
127         }
128     }
129 
130     /**
131      * create a TiledImageCompressionOperation based on a compressed image data.
132      *
133      * @param binaryTable the compressed image data.
134      */
135     public TiledImageCompressionOperation(BinaryTable binaryTable) {
136         super(TileCompressionOperation.class);
137         this.binaryTable = binaryTable;
138     }
139 
140     public void compress(BinaryTableHDU hdu) throws FitsException {
141         processAllTiles();
142         writeColumns(hdu);
143         writeHeader(hdu.getHeader());
144     }
145 
146     @Override
147     public ICompressOption compressOptions() {
148         synchronized (lock) {
149             if (compressorControl == null) {
150                 getCompressorControl();
151                 compressOptions = compressorControl.option();
152                 if (quantAlgorithm != null) {
153                     Header h = new Header();
154                     h.addLine(HeaderCard.create(ZQUANTIZ, quantAlgorithm));
155                     compressOptions.getCompressionParameters().getValuesFromHeader(h);
156                 }
157             }
158             return compressOptions;
159         }
160     }
161 
162     @Override
163     public void ensureColumnParameters() {
164         compressOptions.getCompressionParameters().initializeColumns(getNumberOfTileOperations());
165     }
166 
167     public Buffer decompress() {
168         Buffer decompressedWholeArea = getBaseType().newBuffer(getBufferSize());
169         for (TileCompressionOperation tileOperation : getTileOperations()) {
170             tileOperation.setWholeImageBuffer(decompressedWholeArea);
171         }
172         processAllTiles();
173         decompressedWholeArea.rewind();
174         return decompressedWholeArea;
175     }
176 
177     public void forceNoLoss(int x, int y, int width, int heigth) {
178         TileArea tileArea = new TileArea().start(x, y).end(x + width, y + heigth);
179         for (TileCompressionOperation operation : getTileOperations()) {
180             if (operation.getArea().intersects(tileArea)) {
181                 operation.forceNoLoss(true);
182             }
183         }
184     }
185 
186     @Override
187     public ByteBuffer getCompressedWholeArea() {
188         return compressedWholeArea;
189     }
190 
191     @Override
192     public ICompressorControl getCompressorControl() {
193         synchronized (lock) {
194             if (compressorControl == null) {
195                 compressorControl = CompressorProvider.findCompressorControl(quantAlgorithm, compressAlgorithm,
196                         getBaseType().primitiveClass());
197                 if (compressorControl == null) {
198                     throw new IllegalStateException(
199                             "Found no compressor control for compression algorithm:" + compressAlgorithm + //
200                                     " (quantize algorithm = " + quantAlgorithm + ", base type = "
201                                     + getBaseType().primitiveClass() + ")");
202                 }
203             }
204             return compressorControl;
205         }
206     }
207 
208     @Override
209     public ICompressorControl getGzipCompressorControl() {
210         synchronized (lock) {
211             if (gzipCompressorControl == null) {
212                 gzipCompressorControl = CompressorProvider.findCompressorControl(null, ZCMPTYPE_GZIP_1,
213                         getBaseType().primitiveClass());
214             }
215             return gzipCompressorControl;
216         }
217     }
218 
219     public TiledImageCompressionOperation prepareUncompressedData(final Buffer buffer) throws FitsException {
220         compressedWholeArea = ByteBuffer.wrap(new byte[getBaseType().size() * getBufferSize()]);
221         createTiles(new TileCompressorInitialisation(this, buffer));
222         compressedWholeArea.rewind();
223         return this;
224     }
225 
226     /**
227      * preserve null values, where the value representing null is specified as a parameter. This parameter is ignored
228      * for floating point values where NaN is used as null value.
229      *
230      * @param  nullValue            the value representing null for byte/short and integer pixel values
231      * @param  compressionAlgorithm compression algorithm to use for the null pixel mask
232      *
233      * @return                      the created null pixel mask
234      */
235     public ImageNullPixelMask preserveNulls(long nullValue, String compressionAlgorithm) {
236         imageNullPixelMask = new ImageNullPixelMask(getTileOperations().length, nullValue, compressionAlgorithm);
237         for (TileCompressionOperation tileOperation : getTileOperations()) {
238             tileOperation.createImageNullPixelMask(getImageNullPixelMask());
239         }
240         return imageNullPixelMask;
241     }
242 
243     private void setQuantAlgorithm(final Header header) {
244         synchronized (lock) {
245             // Quantization of floating-point data is signalled by the presence of the ZSCALE and ZZERO columns, which
246             // carry the per-tile scaling used to reconstruct the floating-point values from the stored integers. The
247             // ZQUANTIZ keyword only names the dithering method that was applied *if* the data was quantized; it is not
248             // by itself proof that quantization took place. A losslessly compressed floating-point image (e.g. GZIP_2)
249             // may still carry ZQUANTIZ='NO_DITHER' while storing the raw (byte-shuffled) floating-point values with no
250             // scale/zero columns, and must not be dequantized. Determine quantization from the columns, not the
251             // keyword.
252             boolean hasScale = false;
253             boolean hasZero = false;
254 
255             int nFields = header.getIntValue(TFIELDS);
256 
257             // No quant algorithm unless ZZERO and ZSCALE columns exist.
258             quantAlgorithm = null;
259 
260             for (int i = 1; i <= nFields; i++) {
261                 String type = header.getStringValue(TTYPEn.n(i));
262 
263                 if (ZSCALE_COLUMN.equals(type)) {
264                     hasScale = true;
265                 } else if (ZZERO_COLUMN.equals(type)) {
266                     hasZero = true;
267                 }
268 
269                 if (hasScale && hasZero) {
270                     // Quantized floating-point data. Use the dithering method named by ZQUANTIZ, defaulting to
271                     // NO_DITHER if the keyword is missing or is an unrecognized value.
272                     quantAlgorithm = Compression.ZQUANTIZ_NO_DITHER;
273                     setQuantAlgorithm(header.getCard(ZQUANTIZ));
274                     return;
275                 }
276             }
277         }
278     }
279 
280     public TiledImageCompressionOperation read(final Header header) throws FitsException {
281         readPrimaryHeaders(header);
282         setCompressAlgorithm(header.getCard(ZCMPTYPE));
283         setQuantAlgorithm(header);
284 
285         createTiles(new TileDecompressorInitialisation(this, //
286                 getNullableColumn(header, Object[].class, UNCOMPRESSED_DATA_COLUMN), //
287                 getNullableColumn(header, Object[].class, COMPRESSED_DATA_COLUMN), //
288                 getNullableColumn(header, Object[].class, GZIP_COMPRESSED_DATA_COLUMN), //
289                 header));
290         byte[][] nullPixels = getNullableColumn(header, byte[][].class, NULL_PIXEL_MASK_COLUMN);
291         if (nullPixels != null) {
292             preserveNulls(0L, header.getStringValue(ZMASKCMP)).setColumn(nullPixels);
293         }
294         readCompressionHeaders(header);
295         return this;
296     }
297 
298     public void readPrimaryHeaders(Header header) throws FitsException {
299         readBaseType(header);
300         readAxis(header);
301         readTileAxis(header);
302     }
303 
304     /**
305      * Sets the compression algorithm, via a <code>ZCMPTYPE</code> header card or equivalent. The card must contain one
306      * of the values recognized by the FITS standard. If not, <code>null</code> will be set instead.
307      *
308      * @param  compressAlgorithmCard The header card that specifies the compression algorithm, with one of the standard
309      *                                   recognized values such as {@link Compression#ZCMPTYPE_GZIP_1}, or
310      *                                   <code>null</code>.
311      *
312      * @return                       itself
313      *
314      * @see                          #getCompressAlgorithm()
315      * @see                          #setQuantAlgorithm(HeaderCard)
316      */
317     public TiledImageCompressionOperation setCompressAlgorithm(HeaderCard compressAlgorithmCard) {
318         compressAlgorithm = null;
319 
320         if (compressAlgorithmCard == null) {
321             return this;
322         }
323 
324         String algo = compressAlgorithmCard.getValue().toUpperCase(Locale.US);
325         compressAlgorithm = algo;
326 
327         if (algo.equals(Compression.ZCMPTYPE_RICE_ONE) && FitsFactory.isAllowHeaderRepairs()) {
328             compressAlgorithm = Compression.ZCMPTYPE_RICE_1;
329             Logger.getLogger(HeaderCard.class.getName()).warning("Repaired non-standard ZCMPTYPE value: "
330                     + Compression.ZCMPTYPE_RICE_ONE + " to " + Compression.ZCMPTYPE_RICE_1);
331             return this;
332         }
333 
334         if (algo.equals(Compression.ZCMPTYPE_GZIP_1) || algo.equals(Compression.ZCMPTYPE_GZIP_2)
335                 || algo.equals(Compression.ZCMPTYPE_RICE_1) || algo.equals(Compression.ZCMPTYPE_PLIO_1)
336                 || algo.equals(Compression.ZCMPTYPE_HCOMPRESS_1) || algo.equals(Compression.ZCMPTYPE_NOCOMPRESS)) {
337             return this;
338         }
339 
340         throw new FitsException("Invalid ZCMPTYPE value: " + algo);
341     }
342 
343     /**
344      * Sets the quantization algorithm, via a <code>ZQUANTIZ</code> header card or equivalent. The card must contain one
345      * of the values recognized by the FITS standard. If not, <code>null</code> will be set instead.
346      *
347      * @param  quantAlgorithmCard The header card that specifies the compression algorithm, with one of the standard
348      *                                recognized values such as {@link Compression#ZQUANTIZ_NO_DITHER}, or
349      *                                <code>null</code>.
350      *
351      * @return                    itself
352      *
353      * @see                       #getQuantAlgorithm()
354      * @see                       #setCompressAlgorithm(HeaderCard)
355      */
356     public TiledImageCompressionOperation setQuantAlgorithm(HeaderCard quantAlgorithmCard) {
357         synchronized (lock) {
358             quantAlgorithm = null;
359 
360             if (quantAlgorithmCard == null) {
361                 return this;
362             }
363 
364             String algo = quantAlgorithmCard.getValue().toUpperCase();
365 
366             if (algo.equals(Compression.ZQUANTIZ_NO_DITHER) || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1)
367                     || algo.equals(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_2)) {
368                 quantAlgorithm = algo;
369             } else {
370                 Logger.getLogger(HeaderCard.class.getName()).warning("Ignored invalid ZQUANTIZ value: " + algo);
371             }
372 
373             return this;
374         }
375     }
376 
377     /**
378      * Returns the name of the currently configured quantization algorithm.
379      *
380      * @return The name of the standard quantization algorithm (i.e. a FITS standard value for the <code>ZQUANTIZ</code>
381      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
382      *             value was set before.
383      *
384      * @see    #setQuantAlgorithm(HeaderCard)
385      * @see    #getCompressAlgorithm()
386      *
387      * @since  1.18
388      */
389     public String getQuantAlgorithm() {
390         synchronized (lock) {
391             return quantAlgorithm;
392         }
393     }
394 
395     /**
396      * Returns the name of the currently configured compression algorithm.
397      *
398      * @return The name of the standard compression algorithm (i.e. a FITS standard value for the <code>ZCMPTYPE</code>
399      *             keyword), or <code>null</code> if not quantization is currently defined, possibly because an invalid
400      *             value was set before.
401      *
402      * @see    #setCompressAlgorithm(HeaderCard)
403      * @see    #getQuantAlgorithm()
404      *
405      * @since  1.18
406      */
407     public String getCompressAlgorithm() {
408         return compressAlgorithm;
409     }
410 
411     private <T> T getNullableColumn(Header header, Class<T> class1, String columnName) throws FitsException {
412         for (int i = 1; i <= binaryTable.getNCols(); i++) {
413             String val = header.getStringValue(TTYPEn.n(i));
414             if (val != null && val.trim().equals(columnName)) {
415                 return class1.cast(binaryTable.getColumn(i - 1));
416             }
417         }
418         return null;
419     }
420 
421     private void processAllTiles() {
422         compressOptions();
423         ExecutorService threadPool = FitsFactory.threadPool();
424         for (TileCompressionOperation tileOperation : getTileOperations()) {
425             tileOperation.execute(threadPool);
426         }
427         for (TileCompressionOperation tileOperation : getTileOperations()) {
428             tileOperation.waitForResult();
429         }
430     }
431 
432     private void readAxis(Header header) throws FitsException {
433         if (hasAxes()) {
434             return;
435         }
436         int naxis = header.getIntValue(ZNAXIS);
437         int[] axes = new int[naxis];
438         for (int i = 1; i <= naxis; i++) {
439             int axisValue = header.getIntValue(ZNAXISn.n(i), -1);
440             if (axisValue == -1) {
441                 throw new FitsException("Required ZNAXISn not found");
442             }
443             axes[naxis - i] = axisValue;
444         }
445         setAxes(axes);
446     }
447 
448     private void readBaseType(Header header) {
449         if (getBaseType() == null) {
450             int zBitPix = header.getIntValue(ZBITPIX);
451             ElementType<Buffer> elementType = ElementType.forNearestBitpix(zBitPix);
452             if (elementType == ElementType.UNKNOWN) {
453                 throw new IllegalArgumentException("Illegal value for ZBITPIX: " + zBitPix);
454             }
455             setBaseType(elementType);
456         }
457     }
458 
459     private void readCompressionHeaders(Header header) {
460         compressOptions().getCompressionParameters().getValuesFromHeader(new HeaderAccess(header));
461     }
462 
463     private void readTileAxis(Header header) throws FitsException {
464         if (hasTileAxes()) {
465             return;
466         }
467 
468         int naxes = getNAxes();
469         int[] tileAxes = new int[naxes];
470         // TODO
471         // The FITS default is to tile by row (Pence, W., et al. 2000, ASPC, 216, 551)
472         // However, this library defaulted to full image size by default...
473         for (int i = 1; i <= naxes; i++) {
474             tileAxes[naxes - i] = header.getIntValue(ZTILEn.n(i), i == 1 ? header.getIntValue(ZNAXISn.n(1)) : 1);
475         }
476 
477         setTileAxes(tileAxes);
478     }
479 
480     private <T> Object setInColumn(Object column, boolean predicate, TileCompressionOperation tileOperation, Class<T> clazz,
481             T value) {
482         if (predicate) {
483             if (column == null) {
484                 column = Array.newInstance(clazz, getNumberOfTileOperations());
485             }
486             Array.set(column, tileOperation.getTileIndex(), value);
487         }
488         return column;
489     }
490 
491     private void writeColumns(BinaryTableHDU hdu) throws FitsException {
492         Object compressedColumn = null;
493         Object uncompressedColumn = null;
494         Object gzipColumn = null;
495 
496         synchronized (lock) {
497             for (TileCompressionOperation tileOperation : getTileOperations()) {
498                 TileCompressionType compression = tileOperation.getCompressionType();
499                 byte[] compressedData = tileOperation.getCompressedData();
500 
501                 compressedColumn = setInColumn(compressedColumn, compression == COMPRESSED, tileOperation, byte[].class,
502                         compressedData);
503                 gzipColumn = setInColumn(gzipColumn, compression == GZIP_COMPRESSED, tileOperation, byte[].class,
504                         compressedData);
505                 uncompressedColumn = setInColumn(uncompressedColumn, compression == UNCOMPRESSED, tileOperation,
506                         byte[].class, compressedData);
507             }
508             setNullEntries(compressedColumn, new byte[0]);
509             setNullEntries(gzipColumn, new byte[0]);
510             setNullEntries(uncompressedColumn, new byte[0]);
511             addColumnToTable(hdu, compressedColumn, COMPRESSED_DATA_COLUMN);
512             addColumnToTable(hdu, gzipColumn, GZIP_COMPRESSED_DATA_COLUMN);
513             addColumnToTable(hdu, uncompressedColumn, UNCOMPRESSED_DATA_COLUMN);
514 
515             if (imageNullPixelMask != null) {
516                 addColumnToTable(hdu, imageNullPixelMask.getColumn(), NULL_PIXEL_MASK_COLUMN);
517             }
518             compressOptions.getCompressionParameters().addColumnsToTable(hdu);
519 
520             hdu.getData().fillHeader(hdu.getHeader());
521         }
522     }
523 
524     private void writeHeader(Header header) throws FitsException {
525         HeaderCardBuilder cardBuilder = header//
526                 .card(ZBITPIX).value(getBaseType().bitPix())//
527                 .card(ZCMPTYPE).value(compressAlgorithm);
528         int[] tileAxes = getTileAxes();
529         int naxes = tileAxes.length;
530         for (int i = 1; i <= naxes; i++) {
531             cardBuilder.card(ZTILEn.n(i)).value(tileAxes[naxes - i]);
532         }
533         compressOptions().getCompressionParameters().setValuesInHeader(new HeaderAccess(header));
534         if (imageNullPixelMask != null) {
535             cardBuilder.card(ZMASKCMP).value(imageNullPixelMask.getCompressAlgorithm());
536         }
537     }
538 
539     protected BinaryTable getBinaryTable() {
540         return binaryTable;
541     }
542 
543     protected ImageNullPixelMask getImageNullPixelMask() {
544         return imageNullPixelMask;
545     }
546 
547 }