View Javadoc
1   package nom.tam.image.compression.hdu;
2   
3   import nom.tam.fits.BinaryTable;
4   import nom.tam.fits.BinaryTableHDU;
5   import nom.tam.fits.FitsException;
6   import nom.tam.fits.Header;
7   import nom.tam.fits.HeaderCard;
8   import nom.tam.fits.HeaderCardException;
9   import nom.tam.fits.header.Compression;
10  import nom.tam.fits.header.Standard;
11  import nom.tam.util.Cursor;
12  import nom.tam.util.type.ElementType;
13  
14  /*
15   * #%L
16   * nom.tam FITS library
17   * %%
18   * Copyright (C) 1996 - 2024 nom-tam-fits
19   * %%
20   * This is free and unencumbered software released into the public domain.
21   *
22   * Anyone is free to copy, modify, publish, use, compile, sell, or
23   * distribute this software, either in source code form or as a compiled
24   * binary, for any purpose, commercial or non-commercial, and by any
25   * means.
26   *
27   * In jurisdictions that recognize copyright laws, the author or authors
28   * of this software dedicate any and all copyright interest in the
29   * software to the public domain. We make this dedication for the benefit
30   * of the public at large and to the detriment of our heirs and
31   * successors. We intend this dedication to be an overt act of
32   * relinquishment in perpetuity of all present and future rights to this
33   * software under copyright law.
34   *
35   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
36   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
37   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
38   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
39   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
40   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
41   * OTHER DEALINGS IN THE SOFTWARE.
42   * #L%
43   */
44  
45  import static nom.tam.fits.header.Compression.ZTABLE;
46  
47  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
48  
49  /**
50   * A header-data unit (HDU) containing a compressed binary table. A compressed table is still a binary table but with
51   * some additional constraints. The original table is divided into groups of rows (tiles) and each tile is compressed on
52   * its own. The compressed data is then stored in the 3 data columns of this binary table (compressed, gzipped and
53   * uncompressed) depending on the compression type used in the tile. Additional data columns may contain specific
54   * compression options for each tile (i.e. compressed table row) individually. Table keywords, which conflict with those
55   * in the original table are 'saved' under standard alternative names, so they may be restored with the original table
56   * as appropriate.
57   * <p>
58   * Compressing a table HDU is typically a two-step process:
59   * </p>
60   * <ol>
61   * <li>Create a <code>CompressedTableHDU</code>, e.g. with {@link #fromBinaryTableHDU(BinaryTableHDU, int, String...)},
62   * using the specified number of table rows per compressed block, and compression algorithm(s)</li>
63   * <li>Perform the compression via {@link #compress()}</li>
64   * </ol>
65   * <p>
66   * For example to compress a binary table:
67   * </p>
68   * 
69   * <pre>
70   *   BinaryTableHDU table = ...
71   *   
72   *   // 1. Create compressed HDU with the
73   *   CompressedTableHDU compressed = CompressedTableHDU.fromBinaryTableHDU(table, 4, Compression.ZCMPTYPE_RICE_1);
74   *   
75   *   // 2. Perform the compression.
76   *   compressed.compress();
77   * </pre>
78   * <p>
79   * which of course you can compact into a single line as:
80   * </p>
81   * 
82   * <pre>
83   * CompressedTableHDU compressed = CompressedTableHDU.fromBinaryTableHDU(table, 4, Compression.ZCMPTYPE_RICE_1).compress();
84   * </pre>
85   * <p>
86   * The two step process (as opposed to a single-step one) was probably chosen because it mimics that of
87   * {@link CompressedImageHDU}, where further configuration steps may be inserted in-between. After the compression, the
88   * compressed table HDU can be handled just like any other HDU, and written to a file or stream, for example.
89   * </p>
90   * <p>
91   * The reverse process is simply via the {@link #asBinaryTableHDU()} method. E.g.:
92   * </p>
93   * 
94   * <pre>
95   *    CompressedTableHDU compressed = ...
96   *    BinaryTableHDU table = compressed.asBinaryTableHDU();
97   * </pre>
98   *
99   * @see CompressedTableData
100  */
101 @SuppressWarnings("deprecation")
102 public class CompressedTableHDU extends BinaryTableHDU {
103 
104     private static boolean reversedVLAIndices = false;
105 
106     /**
107      * Enables or disables using reversed compressed/uncompressed heap indices for when decompressing variable-length
108      * arrays (VLAs), as was prescribed by the original FITS 4.0 standard and the Pence et al. 2013 convention.
109      * <p>
110      * The <a href="https://fits.gsfc.nasa.gov/standard40/fits_standard40aa-le.pdf">FITS 4.0 standard</a> defines the
111      * convention of compressing variable-length columns in binary tables. On page 50 it writes:
112      * </p>
113      * <blockquote> 2. Append the VLA descriptors from the uncompressed table (which may be either Q-type or P-type) to
114      * the temporary array of VLA descriptors for the compressed table. </blockquote>
115      * <p>
116      * And the original <a href="https://fits.gsfc.nasa.gov/registry/tiletablecompression/tiletable2.0.pdf">Tiled-table
117      * convention</a> by W. Pence et al. also writes:
118      * </p>
119      * <blockquote> [...] we concatenate the array of descriptors from the uncompressed table onto the end of the
120      * temporary array of descriptors (to the compressed VLAs in the compressed table) before the 2 combined arrays of
121      * descriptors are compressed and written into the heap in the compressed table. </blockquote> However, it appears
122      * that the commonly used tools <code>fpack</code> / <code>funpack</code> provided in CFITSIO do just the reverse of
123      * this presciption. They store the uncopressed pointer <i>before</i> the compressed pointers. Because these tools
124      * are widely used we want to support files created or consumed by these tools. As such we allow to deviate from the
125      * FITS standard, and swap the order of the stored VLA indices inside compressed tables.
126      * 
127      * @param  value <code>true</code> if we should use VLA indices in compressed tables that follow the format
128      *                   described in the original Pence et al. 2013 convention, and also the FITS 4.0 standard as of
129      *                   2024 Mar 1; otherwise <code>false</code>. These original prescriptions define the reverse of
130      *                   what is actually implemented by CFITSIO and its tools <code>fpack</code> and
131      *                   <code>funpack</code>. (Our default is to conform to CFITSIO, and the expected revision of the
132      *                   standard to match).
133      * 
134      * @see          #hasOldStandardVLAIndexing()
135      * @see          #asBinaryTableHDU()
136      * 
137      * @since        1.19.1
138      * 
139      * @author       Attila Kovacs
140      */
141     public static void useOldStandardVLAIndexing(boolean value) {
142         reversedVLAIndices = value;
143     }
144 
145     /**
146      * Checks if we should use reversed compressed/uncompressed heap indices for when decompressing variable-length
147      * arrays (VLAs).
148      * 
149      * @return value <code>true</code> if we will assime VLA indices in compressed tables that follow the format
150      *             described in the original Pence et al. 2013 convention, and also the FITS 4.0 standard as of 2024 Mar
151      *             1; otherwise <code>false</code>. These original prescriptions define the reverse of what is actually
152      *             implemented by CFITSIO and its tools <code>fpack</code> and <code>funpack</code>. (Our default is to
153      *             conform to CFITSIO, and the expcted revision of the standard to match).
154      * 
155      * @see    #useOldStandardVLAIndexing(boolean)
156      * 
157      * @since  1.19.1
158      * 
159      * @author Attila Kovacs
160      */
161     public static boolean hasOldStandardVLAIndexing() {
162         return reversedVLAIndices;
163     }
164 
165     /**
166      * Prepare a compressed binary table HDU for the specified binary table. When the tile row size is specified with
167      * -1, the value will be set ti the number of rows in the table. The table will be compressed in "rows" that are
168      * defined by the tile size. Next step would be to set the compression options into the HDU and then compress it.
169      *
170      * @param  binaryTableHDU              the binary table to compress
171      * @param  tileRows                    the number of rows that should be compressed per tile.
172      * @param  columnCompressionAlgorithms the compression algorithms to use for the columns (optional default
173      *                                         compression will be used if a column has no compression specified). You
174      *                                         should typically use one or more of the enum values defined in
175      *                                         {@link Compression}. The FITS standard currently allows only the lossless
176      *                                         GZIP_1, GZIP_2, RICE_1, or NOCOMPRESS (e.g.
177      *                                         {@link Compression#ZCMPTYPE_GZIP_1} or
178      *                                         {@link Compression#ZCMPTYPE_NOCOMPRESS}.)
179      *
180      * @return                             the prepared compressed binary table HDU.
181      *
182      * @throws IllegalArgumentException    if any of the listed compression algorithms are not approved for use with
183      *                                         tables.
184      * @throws FitsException               if the binary table could not be used to create a compressed binary table.
185      * 
186      * @see                                Compression
187      * @see                                Compression#ZCMPTYPE_GZIP_1
188      * @see                                Compression#ZCMPTYPE_GZIP_2
189      * @see                                Compression#ZCMPTYPE_RICE_1
190      * @see                                Compression#ZCMPTYPE_NOCOMPRESS
191      * @see                                #asBinaryTableHDU()
192      * @see                                #useOldStandardVLAIndexing(boolean)
193      */
194     public static CompressedTableHDU fromBinaryTableHDU(BinaryTableHDU binaryTableHDU, int tileRows,
195             String... columnCompressionAlgorithms) throws FitsException {
196 
197         Header header = new Header();
198 
199         CompressedTableData compressedData = new CompressedTableData();
200         compressedData.setColumnCompressionAlgorithms(columnCompressionAlgorithms);
201 
202         int rowsPerTile = tileRows > 0 ? tileRows : binaryTableHDU.getData().getNRows();
203         compressedData.setRowsPerTile(rowsPerTile);
204 
205         Cursor<String, HeaderCard> headerIterator = header.iterator();
206         Cursor<String, HeaderCard> imageIterator = binaryTableHDU.getHeader().iterator();
207         while (imageIterator.hasNext()) {
208             HeaderCard card = imageIterator.next();
209             CompressedCard.restore(card, headerIterator);
210         }
211 
212         CompressedTableHDU compressedHDU = new CompressedTableHDU(header, compressedData);
213         compressedData.prepareUncompressedData(binaryTableHDU.getData());
214         compressedData.fillHeader(header);
215 
216         return compressedHDU;
217     }
218 
219     /**
220      * Check that this HDU has a valid header for this type.
221      * 
222      * @deprecated     (<i>for internal use</i>) Will reduce visibility in the future
223      *
224      * @param      hdr header to check
225      *
226      * @return         <CODE>true</CODE> if this HDU has a valid header.
227      */
228     @Deprecated
229     @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility")
230     public static boolean isHeader(Header hdr) {
231         return hdr.getBooleanValue(ZTABLE, false);
232     }
233 
234     /**
235      * @deprecated     (<i>for internal use</i>) Will reduce visibility in the future
236      * 
237      * @param      hdr the header that describes the compressed HDU
238      * 
239      * @return         a new blank data object created from the header description
240      */
241     @Deprecated
242     public static CompressedTableData manufactureData(Header hdr) throws FitsException {
243         return new CompressedTableData(hdr);
244     }
245 
246     /**
247      * Creates an new compressed table HDU with the specified header and compressed data.
248      * 
249      * @param hdr   the header
250      * @param datum the compressed table data. The data may not be actually compressed at this point, int which case you
251      *                  may need to call {@link #compress()} before writing the new compressed HDU to a stream.
252      * 
253      * @see         #compress()
254      */
255     public CompressedTableHDU(Header hdr, CompressedTableData datum) {
256         super(hdr, datum);
257     }
258 
259     /**
260      * Restores the original binary table HDU by decompressing the data contained in this compresed table HDU.
261      * 
262      * @return               The uncompressed binary table HDU.
263      * 
264      * @throws FitsException If there was an issue with the decompression.
265      * 
266      * @see                  #asBinaryTableHDU(int, int)
267      * @see                  #fromBinaryTableHDU(BinaryTableHDU, int, String...)
268      * @see                  #useOldStandardVLAIndexing(boolean)
269      */
270     public BinaryTableHDU asBinaryTableHDU() throws FitsException {
271         return asBinaryTableHDU(0, getTileCount());
272         // Header header = getTableHeader();
273         // BinaryTable data = BinaryTableHDU.manufactureData(header);
274         // BinaryTableHDU tableHDU = new BinaryTableHDU(header, data);
275         // getData().asBinaryTable(data, getHeader(), header);
276         // return tableHDU;
277     }
278 
279     /**
280      * Returns the number of table rows that are compressed in each table tile. This may be useful for figuring out what
281      * tiles to decompress, e.g. via {@link #asBinaryTableHDU(int, int)}, when wanting to access select table rows only.
282      * This value is stored under the FITS keyword ZTILELEN in the compressed header. Thus, this method simply provides
283      * a user-friendly way to access it. Note that the last tile may contain fewer rows than the value indicated by this
284      * 
285      * @return               the number of table rows that are compressed into a tile.
286      * 
287      * @throws FitsException if the compressed header does not contain the required ZTILELEN keyword, or it is &lt;= 0.
288      * 
289      * @see                  #asBinaryTableHDU(int, int)
290      * 
291      * @since                1.19
292      */
293     public int getTileRows() throws FitsException {
294         int n = getHeader().getIntValue(Compression.ZTILELEN, -1);
295         if (n <= 0) {
296             throw new FitsException("imnvalid or missing ZTILELEN header keyword");
297         }
298         return n;
299     }
300 
301     /**
302      * Returns the number of compressed tiles contained in this HDU.
303      * 
304      * @return the number of compressed tiles in this table. It is the same as the NAXIS2 value of the header, which is
305      *             also returned by {@link #getNRows()} for this compressed table.
306      * 
307      * @see    #getTileRows()
308      * 
309      * @since  1.19
310      */
311     public int getTileCount() {
312         return getNRows();
313     }
314 
315     /**
316      * Restores a section of the original binary table HDU by decompressing a selected range of compressed table tiles.
317      * The returned section will start at row index <code>fromTile * getTileRows()</code> of the full table.
318      * 
319      * @param  fromTile                 Java index of first tile to decompress
320      * @param  toTile                   Java index of last tile to decompress
321      * 
322      * @return                          The uncompressed binary table HDU from the selected compressed tiles.
323      * 
324      * @throws IllegalArgumentException If the tile range is out of bounds
325      * @throws FitsException            If there was an issue with the decompression.
326      * 
327      * @see                             #getTileRows()
328      * @see                             #getTileCount()
329      * @see                             #asBinaryTableHDU()
330      * @see                             #fromBinaryTableHDU(BinaryTableHDU, int, String...)
331      * @see                             #useOldStandardVLAIndexing(boolean)
332      */
333     public BinaryTableHDU asBinaryTableHDU(int fromTile, int toTile) throws FitsException, IllegalArgumentException {
334         Header header = getTableHeader();
335         int tileSize = getTileRows();
336         getData().setRowsPerTile(tileSize);
337 
338         if (fromTile < 0 || toTile > getTileCount() || toTile <= fromTile) {
339             throw new IllegalArgumentException(
340                     "illegal tile range [" + fromTile + ", " + toTile + "] for " + getTileCount() + " tiles");
341         }
342 
343         // Set the correct number of rows
344         int rows = getHeader().getIntValue(Compression.ZNAXISn.n(2));
345         header.addValue(Standard.NAXIS2, Integer.min(rows, toTile * tileSize) - fromTile * tileSize);
346 
347         BinaryTable data = BinaryTableHDU.manufactureData(header);
348         BinaryTableHDU tableHDU = new BinaryTableHDU(header, data);
349         getData().asBinaryTable(data, getHeader(), header, fromTile);
350 
351         return tableHDU;
352     }
353 
354     /**
355      * Restores a section of the original binary table HDU by decompressing a single compressed table tile. The returned
356      * section will start at row index <code>tile * getTileRows()</code> of the full table.
357      * 
358      * @param  tile                     Java index of the table tile to decompress
359      * 
360      * @return                          The uncompressed binary table HDU from the selected compressed tile.
361      * 
362      * @throws IllegalArgumentException If the tile index is out of bounds
363      * @throws FitsException            If there was an issue with the decompression.
364      * 
365      * @see                             #asBinaryTableHDU(int, int)
366      * @see                             #getTileRows()
367      * @see                             #getTileCount()
368      * @see                             #useOldStandardVLAIndexing(boolean)
369      */
370     public final BinaryTableHDU asBinaryTableHDU(int tile) throws FitsException, IllegalArgumentException {
371         return asBinaryTableHDU(tile, tile + 1);
372     }
373 
374     /**
375      * Returns a particular section of a decompressed data column.
376      * 
377      * @param  col                      the Java column index
378      * 
379      * @return                          The uncompressed column data as an array.
380      * 
381      * @throws IllegalArgumentException If the tile range is out of bounds
382      * @throws FitsException            If there was an issue with the decompression.
383      * 
384      * @see                             #getColumnData(int, int, int)
385      * @see                             #asBinaryTableHDU()
386      * @see                             #fromBinaryTableHDU(BinaryTableHDU, int, String...)
387      */
388     public Object getColumnData(int col) throws FitsException, IllegalArgumentException {
389         return getColumnData(col, 0, getTileCount());
390     }
391 
392     /**
393      * Returns a particular section of a decompressed data column.
394      * 
395      * @param  col                      the Java column index
396      * @param  fromTile                 the Java index of first tile to decompress
397      * @param  toTile                   the Java index of last tile to decompress
398      * 
399      * @return                          The uncompressed column data segment as an array.
400      * 
401      * @throws IllegalArgumentException If the tile range is out of bounds
402      * @throws FitsException            If there was an issue with the decompression.
403      * 
404      * @see                             #getColumnData(int)
405      * @see                             #asBinaryTableHDU()
406      * @see                             #fromBinaryTableHDU(BinaryTableHDU, int, String...)
407      */
408     public Object getColumnData(int col, int fromTile, int toTile) throws FitsException, IllegalArgumentException {
409         getData().setRowsPerTile(getTileRows());
410         return getData().getColumnData(col, fromTile, toTile, getHeader(), getTableHeader());
411     }
412 
413     /**
414      * Performs the actual compression with the selected algorithm(s) and options. When creating a compressed table HDU,
415      * e.g using the {@link #fromBinaryTableHDU(BinaryTableHDU, int, String...)} method, the HDU is merely prepared but
416      * without actually performing the compression, and this method will have to be called to actually perform the
417      * compression. The design would allow for setting options between creation and compressing, but in this case there
418      * is really nothing of the sort.
419      * 
420      * @return               itself
421      * 
422      * @throws FitsException if the compression could not be performed
423      * 
424      * @see                  #fromBinaryTableHDU(BinaryTableHDU, int, String...)
425      * @see                  #useOldStandardVLAIndexing(boolean)
426      */
427     public CompressedTableHDU compress() throws FitsException {
428         getData().compress(getHeader());
429         return this;
430     }
431 
432     /**
433      * Obtain a header representative of a decompressed TableHDU.
434      *
435      * @return                     Header with decompressed cards.
436      *
437      * @throws HeaderCardException if the card could not be copied
438      *
439      * @since                      1.18
440      */
441     public Header getTableHeader() throws HeaderCardException {
442         Header header = new Header();
443 
444         header.addValue(Standard.XTENSION, Standard.XTENSION_BINTABLE);
445         header.addValue(Standard.BITPIX, ElementType.BYTE.bitPix());
446         header.addValue(Standard.NAXIS, 2);
447 
448         Cursor<String, HeaderCard> tableIterator = header.iterator();
449         Cursor<String, HeaderCard> iterator = getHeader().iterator();
450 
451         while (iterator.hasNext()) {
452             CompressedCard.backup(iterator.next(), tableIterator);
453         }
454 
455         return header;
456     }
457 
458     @Override
459     public CompressedTableData getData() {
460         return (CompressedTableData) super.getData();
461     }
462 
463 }