1 package nom.tam.image.compression.hdu;
2
3 import java.io.IOException;
4 import java.nio.Buffer;
5 import java.nio.ByteBuffer;
6 import java.util.Arrays;
7 import java.util.HashMap;
8 import java.util.List;
9 import java.util.Map;
10
11 import nom.tam.fits.BinaryTableHDU;
12 import nom.tam.fits.FitsException;
13 import nom.tam.fits.FitsUtil;
14 import nom.tam.fits.Header;
15 import nom.tam.fits.HeaderCard;
16 import nom.tam.fits.HeaderCardException;
17 import nom.tam.fits.ImageData;
18 import nom.tam.fits.ImageHDU;
19 import nom.tam.fits.compression.algorithm.api.ICompressOption;
20 import nom.tam.fits.header.Compression;
21 import nom.tam.fits.header.GenericKey;
22 import nom.tam.fits.header.IFitsHeader;
23 import nom.tam.fits.header.Standard;
24 import nom.tam.image.compression.CompressedImageTiler;
25 import nom.tam.util.ByteBufferInputStream;
26 import nom.tam.util.ByteBufferOutputStream;
27 import nom.tam.util.Cursor;
28 import nom.tam.util.FitsInputStream;
29 import nom.tam.util.FitsOutputStream;
30
31 /*
32 * #%L
33 * nom.tam FITS library
34 * %%
35 * Copyright (C) 1996 - 2024 nom-tam-fits
36 * %%
37 * This is free and unencumbered software released into the public domain.
38 *
39 * Anyone is free to copy, modify, publish, use, compile, sell, or
40 * distribute this software, either in source code form or as a compiled
41 * binary, for any purpose, commercial or non-commercial, and by any
42 * means.
43 *
44 * In jurisdictions that recognize copyright laws, the author or authors
45 * of this software dedicate any and all copyright interest in the
46 * software to the public domain. We make this dedication for the benefit
47 * of the public at large and to the detriment of our heirs and
48 * successors. We intend this dedication to be an overt act of
49 * relinquishment in perpetuity of all present and future rights to this
50 * software under copyright law.
51 *
52 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
53 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
54 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
55 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
56 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
57 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
58 * OTHER DEALINGS IN THE SOFTWARE.
59 * #L%
60 */
61
62 import static nom.tam.fits.header.Compression.ZIMAGE;
63 import static nom.tam.fits.header.Standard.BLANK;
64
65 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
66
67 /**
68 * <p>
69 * A header-data unit (HDU) containing a compressed image. A compressed image is a normal binary table with some
70 * additional constraints. The original image is divided into tiles and each tile is compressed on its own. The
71 * compressed data is then stored in the 3 data columns of this binary table (compressed, gzipped and uncompressed)
72 * depending on the compression type used in the tile. Additional data columns may contain specific compression or
73 * quantization options for each tile (i.e. compressed table row) individually. Table keywords, which conflict with
74 * those in the original image are 'saved' under standard alternative names, so they may be restored with the image as
75 * appropriate.
76 * </p>
77 * <p>
78 * Compressing an image HDU is typically a multi-step process:
79 * </p>
80 * <ol>
81 * <li>Create a <code>CompressedImageHDU</code>, e.g. with {@link #fromImageHDU(ImageHDU, int...)}.</li>
82 * <li>Set up the compression algorithm, including quantization (if desired) via {@link #setCompressAlgorithm(String)}
83 * and {@link #setQuantAlgorithm(String)}, and optionally the compressiomn method used for preserving the blank values
84 * via {@link #preserveNulls(String)}.</li>
85 * <li>Set compression (and quantization) options, via calling on {@link #getCompressOption(Class)}</li>
86 * <li>Perform the compression via {@link #compress()}</li>
87 * </ol>
88 * <p>
89 * For example to compress an image HDU:
90 * </p>
91 *
92 * <pre>
93 * ImageHDU image = ...
94 *
95 * // 1. Create compressed HDU
96 * CompressedImageHDU compressed = CompressedImageHDU.fromImageHDU(image, 60, 40);
97 *
98 * // 2. Set compression (and optional qunatizaiton) algorithm(s)
99 * compressed.setCompressAlgorithm(Compression.ZCMPTYPE_RICE_1)
100 * .setQuantAlgorithm(Compression.ZQUANTIZ_SUBTRACTIVE_DITHER_1)
101 * .preserveNulls(Compression.ZCMPTYPE_HCOMPRESS_1);
102 *
103 * // 3. Set compression (and quantizaiton) options
104 * compressed.getCompressOption(RiceCompressOption.class).setBlockSize(32);
105 * compressed.getCompressOption(QuantizeOption.class).setBZero(3.0).setBScale(0.1).setBNull(-999);
106 *
107 * // 4. Perform the compression.
108 * compressed.compress();
109 * </pre>
110 * <p>
111 * After the compression, the compressed image HDU can be handled just like any other HDU, and written to a file or
112 * stream, for example.
113 * </p>
114 * <p>
115 * The reverse process is simply via the {@link #asImageHDU()} method. E.g.:
116 * </p>
117 *
118 * <pre>
119 * CompressedImageHDU compressed = ...
120 * ImageHDU image = compressed.asImageHDU();
121 * </pre>
122 *
123 * @see CompressedImageData
124 * @see nom.tam.image.compression.CompressedImageTiler
125 */
126 @SuppressWarnings("deprecation")
127 public class CompressedImageHDU extends BinaryTableHDU {
128 /** The maximum number of table columns FITS supports */
129 public static final int MAX_NAXIS_ALLOWED = 999;
130
131 /**
132 * keys that are only valid in tables and should not go into the uncompressed image.
133 */
134 private static final List<IFitsHeader> TABLE_COLUMN_KEYS = Arrays.asList(binaryTableColumnKeyStems());
135
136 static final Map<IFitsHeader, CompressedCard> COMPRESSED_HEADER_MAPPING = new HashMap<>();
137
138 static final Map<IFitsHeader, CompressedCard> UNCOMPRESSED_HEADER_MAPPING = new HashMap<>();
139
140 /**
141 * Prepare a compressed image hdu for the specified image. the tile axis that are specified with -1 default to
142 * tiling by rows. To actually perform the compression, you will next have to select the compression algorithm (and
143 * optinally a quantization algorithm), then configure options for these, and finally call {@link #compress()} to
144 * perform the compression. See the description of this class for more details.
145 *
146 * @param imageHDU the image to compress
147 * @param tileAxis the requested tile sizes in pixels in x, y, z... order (i.e. opposite of the Java array
148 * indexing order!). The actual tile sizes that are set might be different, e.g. to fit
149 * within the image bounds and/or to conform to tiling conventions (esp. in more than 2
150 * dimensions).
151 *
152 * @return the prepared compressed image hdu.
153 *
154 * @throws FitsException if the image could not be used to create a compressed image.
155 *
156 * @see #asImageHDU()
157 * @see #setCompressAlgorithm(String)
158 * @see #setQuantAlgorithm(String)
159 * @see #getCompressOption(Class)
160 * @see #compress()
161 */
162 public static CompressedImageHDU fromImageHDU(ImageHDU imageHDU, int... tileAxis) throws FitsException {
163 Header header = new Header();
164 CompressedImageData compressedData = new CompressedImageData();
165 int[] size = imageHDU.getAxes();
166 int[] tileSize = new int[size.length];
167
168 compressedData.setAxis(size);
169
170 // Start with the default tile size.
171 int nm1 = size.length - 1;
172 Arrays.fill(tileSize, 1);
173 tileSize[nm1] = size[nm1];
174
175 // Check and apply the requested tile sizes.
176 int n = Math.min(size.length, tileAxis.length);
177 for (int i = 0; i < n; i++) {
178 if (tileAxis[i] > 0) {
179 tileSize[nm1 - i] = Math.min(tileAxis[i], size[nm1 - i]);
180 }
181 }
182
183 compressedData.setTileSize(tileSize);
184
185 compressedData.fillHeader(header);
186 Cursor<String, HeaderCard> iterator = header.iterator();
187 Cursor<String, HeaderCard> imageIterator = imageHDU.getHeader().iterator();
188 while (imageIterator.hasNext()) {
189 HeaderCard card = imageIterator.next();
190 CompressedCard.restore(card, iterator);
191 }
192 CompressedImageHDU compressedImageHDU = new CompressedImageHDU(header, compressedData);
193 compressedData.prepareUncompressedData(imageHDU.getData().getData(), header);
194 return compressedImageHDU;
195 }
196
197 /**
198 * Check that this HDU has a valid header for this type.
199 *
200 * @deprecated (<i>for internal use</i>) Will reduce visibility in the future
201 *
202 * @param hdr header to check
203 *
204 * @return <CODE>true</CODE> if this HDU has a valid header.
205 */
206 @Deprecated
207 @SuppressFBWarnings(value = "HSM_HIDING_METHOD", justification = "deprecated existing method, kept for compatibility")
208 public static boolean isHeader(Header hdr) {
209 return hdr.getBooleanValue(ZIMAGE, false);
210 }
211
212 /**
213 * Returns an empty compressed image data object based on its description in a FITS header.
214 *
215 * @param hdr the FITS header containing a description of the compressed image
216 *
217 * @return an empty compressed image data corresponding to the header description.
218 *
219 * @throws FitsException if the header does not sufficiently describe a compressed image
220 *
221 * @deprecated (<i>for internal use</i>) Will reduce visibility in the future
222 */
223 @Deprecated
224 public static CompressedImageData manufactureData(Header hdr) throws FitsException {
225 return new CompressedImageData(hdr);
226 }
227
228 /**
229 * Creates an new compressed image HDU with the specified header and compressed data.
230 *
231 * @param hdr the header
232 * @param datum the compressed image data. The data may not be actually compressed at this point, int which case you
233 * may need to call {@link #compress()} before writing the new compressed HDU to a stream.
234 *
235 * @see #compress()
236 */
237 public CompressedImageHDU(Header hdr, CompressedImageData datum) {
238 super(hdr, datum);
239 }
240
241 /**
242 * Restores the original image HDU by decompressing the data contained in this compresed image HDU.
243 *
244 * @return The uncompressed Image HDU.
245 *
246 * @throws FitsException If there was an issue with the decompression.
247 *
248 * @see #getTileHDU(int[], int[])
249 * @see #fromImageHDU(ImageHDU, int...)
250 */
251 public ImageHDU asImageHDU() throws FitsException {
252 final Header header = getImageHeader();
253 ImageData data = ImageHDU.manufactureData(header);
254 ImageHDU imageHDU = new ImageHDU(header, data);
255 data.setBuffer(getUncompressedData());
256 return imageHDU;
257 }
258
259 /**
260 * Returns an <code>ImageHDU</code>, with the specified decompressed image area. The HDU's header will be adjusted
261 * as necessary to reflect the correct size and coordinate system of the image cutout.
262 *
263 * @param corners the location in pixels where the tile begins in the full (uncompressed) image.
264 * The number of elements in the array must match the image dimnesion.
265 * @param lengths the size of the tile in pixels. The number of elements in the array must match
266 * the image dimnesion.
267 *
268 * @return a new image HDU containing the selected area of the uncompresed image, including
269 * the adjusted header for the selected area,
270 *
271 * @throws IOException If the tiling operation itself could not be performed
272 * @throws FitsException If the compressed image itself if invalid or imcomplete
273 * @throws IllegalArgumentException if the tile area is not fully contained inside the uncompressed image or if the
274 * lengths are not positive definite.
275 *
276 * @see #asImageHDU()
277 * @see CompressedImageTiler#getTile(int[], int[])
278 *
279 * @since 1.18
280 */
281 public ImageHDU getTileHDU(int[] corners, int[] lengths) throws IOException, FitsException, IllegalArgumentException {
282 Header h = getImageHeader();
283
284 int dim = h.getIntValue(Standard.NAXIS);
285
286 if (corners.length != lengths.length || corners.length != dim) {
287 throw new IllegalArgumentException("arguments for mismatched dimensions");
288 }
289
290 // Edit the image bound for the tile
291 for (int i = 0; i < corners.length; i++) {
292 int naxis = h.getIntValue(Standard.NAXISn.n(dim - i));
293
294 if (lengths[0] <= 0) {
295 throw new IllegalArgumentException("Illegal tile size in dim " + i + ": " + lengths[i]);
296 }
297
298 if (corners[i] < 0 || corners[i] + lengths[i] > naxis) {
299 throw new IllegalArgumentException("tile out of bounds in dim " + i + ": [" + corners[i] + ":"
300 + (corners[i] + lengths[i]) + "] in " + naxis);
301 }
302
303 h.addValue(Standard.NAXISn.n(dim - i), lengths[i]);
304
305 // Adjust the CRPIXn values
306 HeaderCard crpix = h.getCard(Standard.CRPIXn.n(dim - i));
307 if (crpix != null) {
308 crpix.setValue(crpix.getValue(Double.class, Double.NaN) - corners[i]);
309 }
310
311 // Adjust CRPIXna values
312 for (char c = 'A'; c <= 'Z'; c++) {
313 crpix = h.getCard("CRPIX" + (dim - i) + Character.toString(c));
314 if (crpix != null) {
315 crpix.setValue(crpix.getValue(Double.class, Double.NaN) - corners[i]);
316 }
317 }
318 }
319
320 ImageData im = ImageHDU.manufactureData(h);
321 ByteBuffer buf = ByteBuffer.wrap(new byte[(int) FitsUtil.addPadding(im.getSize())]);
322
323 try (FitsOutputStream out = new FitsOutputStream(new ByteBufferOutputStream(buf))) {
324 new CompressedImageTiler(this).getTile(out, corners, lengths);
325 out.close();
326 }
327
328 // Rewind buffer for reading, including padding.
329 buf.limit(buf.capacity());
330 buf.position(0);
331
332 try (FitsInputStream in = new FitsInputStream(new ByteBufferInputStream(buf))) {
333 im.read(in);
334 in.close();
335 }
336
337 return new ImageHDU(h, im);
338 }
339
340 /**
341 * Given this compressed HDU, get the original (decompressed) axes.
342 *
343 * @return the dimensions of the axis.
344 *
345 * @throws FitsException if the axis are configured wrong.
346 *
347 * @since 1.18
348 */
349 public int[] getImageAxes() throws FitsException {
350 int nAxis = myHeader.getIntValue(Compression.ZNAXIS);
351 if (nAxis < 0) {
352 throw new FitsException("Negative ZNAXIS (or NAXIS) value " + nAxis);
353 }
354 if (nAxis > CompressedImageHDU.MAX_NAXIS_ALLOWED) {
355 throw new FitsException("ZNAXIS/NAXIS value " + nAxis + " too large");
356 }
357
358 if (nAxis == 0) {
359 return null;
360 }
361
362 final int[] axes = new int[nAxis];
363 for (int i = 1; i <= nAxis; i++) {
364 axes[nAxis - i] = myHeader.getIntValue(Compression.ZNAXISn.n(i));
365 }
366
367 return axes;
368 }
369
370 /**
371 * Obtain a header representative of a decompressed ImageHDU.
372 *
373 * @return Header with decompressed cards.
374 *
375 * @throws HeaderCardException if the card could not be copied
376 *
377 * @since 1.18
378 */
379 public Header getImageHeader() throws HeaderCardException {
380 Header header = new Header();
381
382 Cursor<String, HeaderCard> imageIterator = header.iterator();
383 Cursor<String, HeaderCard> iterator = getHeader().iterator();
384
385 while (iterator.hasNext()) {
386 HeaderCard card = iterator.next();
387
388 if (!TABLE_COLUMN_KEYS.contains(GenericKey.lookup(card.getKey()))) {
389 CompressedCard.backup(card, imageIterator);
390 }
391 }
392 return header;
393 }
394
395 /**
396 * Performs the actual compression with the selected algorithm(s) and options. When creating a compressed image HDU,
397 * e.g using the {@link #fromImageHDU(ImageHDU, int...)} method, the HDU is merely prepared but without actually
398 * performing the compression to allow the user to configure the algorithm(S) to be used as well as any specific
399 * compression (or quantization) options. See details in the class description.
400 *
401 * @throws FitsException if the compression could not be performed
402 *
403 * @see #fromImageHDU(ImageHDU, int...)
404 * @see #setCompressAlgorithm(String)
405 * @see #setQuantAlgorithm(String)
406 * @see #getCompressOption(Class)
407 */
408 public void compress() throws FitsException {
409 getData().compress(this);
410 }
411
412 /**
413 * Specify an area within the image that will not undergo a lossy compression. This will only have affect it the
414 * selected compression (including the options) is a lossy compression. All tiles touched by this region will be
415 * handled so that there is no loss of any data, the reconstruction will be exact.
416 *
417 * @param x the x position in the image
418 * @param y the y position in the image
419 * @param width the width of the area
420 * @param heigth the height of the area
421 *
422 * @return this
423 */
424 public CompressedImageHDU forceNoLoss(int x, int y, int width, int heigth) {
425 getData().forceNoLoss(x, y, width, heigth);
426 return this;
427 }
428
429 /**
430 * Returns the compression (or quantization) options for a selected compression option class. It is presumed that
431 * the requested options are appropriate for the compression and/or quantization algorithm that was selected. E.g.,
432 * if you called <code>setCompressionAlgorithm({@link Compression#ZCMPTYPE_RICE_1})</code>, then you can retrieve
433 * options for it with this method as
434 * <code>getCompressOption({@link nom.tam.fits.compression.algorithm.rice.RiceCompressOption}.class)</code>.
435 *
436 * @param <T> The generic type of the compression class
437 * @param clazz the compression class
438 *
439 * @return The current set of options for the requested type, or <code>null</code> if there are no options or
440 * if the requested type does not match the algorithm(s) selected.
441 *
442 * @see nom.tam.fits.compression.algorithm.hcompress.HCompressorOption
443 * @see nom.tam.fits.compression.algorithm.rice.RiceCompressOption
444 * @see nom.tam.fits.compression.algorithm.quant.QuantizeOption
445 */
446 public <T extends ICompressOption> T getCompressOption(Class<T> clazz) {
447 return getData().getCompressOption(clazz);
448 }
449
450 @Override
451 public CompressedImageData getData() {
452 return (CompressedImageData) super.getData();
453 }
454
455 /**
456 * Returns the uncompressed image in serialized form, as it would appear in a stream.
457 *
458 * @deprecated (<i>for internal use</i>) There is no reason why this should be exposed to users. Use
459 * {@link #asImageHDU()} instead. Future release may restrict the visibility to
460 * private.
461 *
462 * @return the buffer containing the serialized form of the uncompressed image.
463 *
464 * @throws FitsException if the decompression could not be performed.
465 *
466 * @see #asImageHDU()
467 */
468 @Deprecated
469 public Buffer getUncompressedData() throws FitsException {
470 return getData().getUncompressedData(getHeader());
471 }
472
473 @Override
474 @Deprecated
475 public boolean isHeader() {
476 return super.isHeader() && isHeader(myHeader);
477 }
478
479 /**
480 * Sets the compression algorithm used for preserving the blank values in the original image even if the compression
481 * is lossy. When compression an integer image, a BLANK header should be defined in its header. You should typically
482 * use one of the enum values defined in {@link Compression}.
483 *
484 * @param compressionAlgorithm compression algorithm to use for the null pixel mask, see {@link Compression} for
485 * recognized names.
486 *
487 * @return itself
488 *
489 * @see Compression
490 * @see #setCompressAlgorithm(String)
491 * @see #setQuantAlgorithm(String)
492 */
493 public CompressedImageHDU preserveNulls(String compressionAlgorithm) {
494 long nullValue = getHeader().getLongValue(BLANK, Long.MIN_VALUE);
495 getData().preserveNulls(nullValue, compressionAlgorithm);
496 return this;
497 }
498
499 /**
500 * Sets the compression algorithm to use, by its standard FITS name. You should typically use one of the enum values
501 * defined in {@link Compression}.
502 *
503 * @param compressAlgorithm compression algorithm to use, see {@link Compression} for recognized names.
504 *
505 * @return itself
506 *
507 * @throws FitsException if no algorithm is available by the specified name
508 *
509 * @see Compression
510 * @see #setQuantAlgorithm(String)
511 * @see #preserveNulls(String)
512 */
513 public CompressedImageHDU setCompressAlgorithm(String compressAlgorithm) throws FitsException {
514 HeaderCard compressAlgorithmCard = HeaderCard.create(Compression.ZCMPTYPE, compressAlgorithm);
515 getData().setCompressAlgorithm(compressAlgorithmCard);
516 return this;
517 }
518
519 /**
520 * Sets the quantization algorithm to use, by its standard FITS name. You should typically use one of the enum
521 * values defined in {@link Compression}.
522 *
523 * @param quantAlgorithm quantization algorithm to use, see {@link Compression} for recognized names.
524 *
525 * @return itself
526 *
527 * @throws FitsException if no algorithm is available by the specified name
528 *
529 * @see Compression
530 * @see #setCompressAlgorithm(String)
531 * @see #preserveNulls(String)
532 */
533 public CompressedImageHDU setQuantAlgorithm(String quantAlgorithm) throws FitsException {
534 if (quantAlgorithm != null && !quantAlgorithm.isEmpty()) {
535 HeaderCard quantAlgorithmCard = HeaderCard.create(Compression.ZQUANTIZ, quantAlgorithm);
536 getData().setQuantAlgorithm(quantAlgorithmCard);
537 } else {
538 getData().setQuantAlgorithm(null);
539 }
540 return this;
541 }
542 }