1 package nom.tam.fits;
2
3 /*
4 * #%L
5 * nom.tam FITS library
6 * %%
7 * Copyright (C) 2004 - 2024 nom-tam-fits
8 * %%
9 * This is free and unencumbered software released into the public domain.
10 *
11 * Anyone is free to copy, modify, publish, use, compile, sell, or
12 * distribute this software, either in source code form or as a compiled
13 * binary, for any purpose, commercial or non-commercial, and by any
14 * means.
15 *
16 * In jurisdictions that recognize copyright laws, the author or authors
17 * of this software dedicate any and all copyright interest in the
18 * software to the public domain. We make this dedication for the benefit
19 * of the public at large and to the detriment of our heirs and
20 * successors. We intend this dedication to be an overt act of
21 * relinquishment in perpetuity of all present and future rights to this
22 * software under copyright law.
23 *
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 * OTHER DEALINGS IN THE SOFTWARE.
31 * #L%
32 */
33
34 import java.io.Closeable;
35 import java.io.DataOutput;
36 import java.io.DataOutputStream;
37 import java.io.File;
38 import java.io.FileOutputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.net.URL;
42 import java.nio.file.Files;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.NoSuchElementException;
46 import java.util.Properties;
47 import java.util.logging.Level;
48 import java.util.logging.Logger;
49
50 import nom.tam.fits.compress.CompressionManager;
51 import nom.tam.fits.header.Standard;
52 import nom.tam.fits.utilities.FitsCheckSum;
53 import nom.tam.util.ArrayDataInput;
54 import nom.tam.util.ArrayDataOutput;
55 import nom.tam.util.FitsFile;
56 import nom.tam.util.FitsIO;
57 import nom.tam.util.FitsInputStream;
58 import nom.tam.util.FitsOutputStream;
59 import nom.tam.util.RandomAccess;
60 import nom.tam.util.RandomAccessFileIO;
61 import nom.tam.util.SafeClose;
62
63 import static nom.tam.fits.header.Standard.EXTNAME;
64 import static nom.tam.fits.header.Standard.EXTVER;
65
66 /**
67 * <p>
68 * Handling of FITS files and streams. This class is a container of HDUs (header-data units), which together constitute
69 * a complete FITS file. Users of this library are strongly encouraged to study the
70 * <a href="https://fits.gsfc.nasa.gov/fits_standard.html">FITS Standard</a> documentation before using this library, as
71 * the library typically requires a level of familiarity with FITS and its capabilities. When constructing FITS files,
72 * users will typically want to populate their headers with as much of the standard information as possible to provide a
73 * full and accurate description of the data they wish to represent way beyond the bare essentials that are handled
74 * automatically by this library.
75 * </p>
76 * <p>
77 * <code>Fits</code> objects can be built-up HDU-by-HDU, and then written to a file (or stream), e.g.:
78 * </p>
79 *
80 * <pre>
81 * // Create a new empty Fits containe
82 * Fits fits = new Fits();
83 *
84 * // Create an image HDU, e.g. from a 2D array we have prepared earlier
85 * float[][] image = ...
86 * BasicHDU<?> imageHDU = Fits.makeHDU(image);
87 *
88 * // ... we can of course add data to the HDU's header as we like...
89 *
90 * // Make this image the first HDU...
91 * fits.addHDU(imageHDU);
92 *
93 * // Write the FITS to a file...
94 * fits.write("myimage.fits");
95 * </pre>
96 * <p>
97 * Or, we may read a <code>Fits</code> object from the input, e.g. as:
98 * </p>
99 *
100 * <pre>
101 * // Create and empty Fits assigned to an input file
102 * Fits f = new Fits(new File("myimage.fits");
103 *
104 * // Read the entire FITS (skipping over the data for now...)
105 * f.read();
106 *
107 * // Get the image data from the first HDU (will actually read the image now)
108 * float[][] image = (float[][]) f.getHDU(0).getKernel();
109 * </pre>
110 * <p>
111 * When reading FITS from random-accessible files (like in the example above), the {@link #read()} call will parse the
112 * header for each HDU but will defer reading of actual data to a later time when it's actually accessed. This makes
113 * <code>Fits</code> objects fast, frugal, and lean, especially when one is interested in certain parts of the data
114 * contained in the FITS file. (When reading from streams, deferred reading is not an option, so {@link #read()} will
115 * load all HDUs into memory each time).
116 * </p>
117 * <p>
118 * <code>Fits</code> objects also allow reading HDUs sequentially one at a time using the {@link #readHDU()}, or even
119 * when using {@link #getHDU(int)} or {@link #getHDU(String)} methods, even if {@link #read()} was not called
120 * previously, e.g.:
121 * </p>
122 *
123 * <pre>
124 * // Create and empty Fits assigned to an input
125 * Fits f = new Fits(new File("myimage.fits");
126 *
127 * // Get HDU index 2 (0-based, i.e. 3rd HDU) FITS. It will read (stream) or skim (file) the FITS up to the 3rd
128 * // HDU, returning it. If the FITS file or stream contains further HDUs they will not be accessed until we
129 * // need them later (if at all).
130 * BasucHDU<?> hdu = f.getHDU(2);
131 * </pre>
132 * <p>
133 * When building <code>Fits</code> from local Java data objects, it's best to use {@link #makeHDU(Object)} to create
134 * HDUs, which will chose the most appropriate type of HDU for the given data object (taking into some of the static
135 * preferences set in <code>FitsFactory</code> prior). {@link #makeHDU(Object)} will return one of the following HDU
136 * objects:
137 * <ul>
138 * <li>{@link NullDataHDU}</li>
139 * <li>{@link ImageHDU}</li>
140 * <li>{@link BinaryTableHDU}</li>
141 * <li>{@link AsciiTableHDU}</li>
142 * <li>{@link UndefinedHDU}</li>
143 * </ul>
144 * <p>
145 * all of which derive from {@link BasicHDU}.
146 * </p>
147 * <p>
148 * Since HDU literally means 'header-data unit', they constitute of a header and data entities, which can be accessed
149 * separately. The {@link Header} class provides many functions to add, delete and read header keywords in HDUs in a
150 * variety of formats. The {@link Data} class, and its concrete subclassses provide access to the specific data object
151 * that the HDU encapsulates.
152 * </p>
153 *
154 * @see FitsFactory
155 *
156 * @version 1.21
157 */
158 @SuppressWarnings("deprecation")
159 public class Fits implements Closeable {
160
161 /**
162 * logger to log to.
163 */
164 private static final Logger LOG = Logger.getLogger(Fits.class.getName());
165
166 /**
167 * The input stream associated with this Fits object.
168 */
169 private ArrayDataInput dataStr;
170
171 /**
172 * A vector of HDUs that have been added to this Fits object.
173 */
174 private final List<BasicHDU<?>> hduList = new ArrayList<>();
175
176 /**
177 * Has the input stream reached the EOF?
178 */
179 private boolean atEOF;
180
181 /**
182 * The last offset we reached. A -1 is used to indicate that we cannot use the offset.
183 */
184 private long lastFileOffset = -1;
185
186 /**
187 * Creates an empty Fits object which is not associated with an input stream.
188 */
189 public Fits() {
190 }
191
192 /**
193 * <p>
194 * Creates a new (empty) FITS container associated with a file input. If the file is compressed a stream will be
195 * used, otherwise random access will be supported.
196 * </p>
197 * <p>
198 * While the FITS object is associated with the specified file, it is initialized as an empty container with no data
199 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
200 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
201 * the container.
202 * </p>
203 *
204 * @param myFile The File object. The content of this file will not be read into the Fits object until the
205 * user makes some explicit request. * @throws FitsException if the operation failed
206 *
207 * @throws FitsException if the operation failed
208 *
209 * @see #Fits(FitsFile)
210 * @see #Fits(RandomAccessFileIO)
211 * @see #Fits(String)
212 * @see #read()
213 * @see #getHDU(int)
214 * @see #readHDU()
215 * @see #skipHDU()
216 * @see #addHDU(BasicHDU)
217 */
218 public Fits(File myFile) throws FitsException {
219 this(myFile, CompressionManager.isCompressed(myFile));
220 }
221
222 /**
223 * <p>
224 * Creates a new (empty) FITS container associated with a file input.
225 * </p>
226 * <p>
227 * While the FITS object is associated with the specified file, it is initialized as an empty container with no data
228 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
229 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
230 * the container.
231 * </p>
232 *
233 * @deprecated Use {@link #Fits(File)} instead (compression is auto detected). Will remove in the
234 * future.
235 *
236 * @param myFile The File object. The content of this file will not be read into the Fits object until
237 * the user makes some explicit request.
238 * @param compressed Is the data compressed?
239 *
240 * @throws FitsException if the operation failed
241 *
242 * @see #Fits(File)
243 */
244 @Deprecated
245 public Fits(File myFile, boolean compressed) throws FitsException {
246 fileInit(myFile, compressed);
247 }
248
249 /**
250 * <p>
251 * Creates a new (empty) FITS container associated with an input that supports generalized random access.
252 * </p>
253 * <p>
254 * While the FITS object is associated with the specified input, it is initialized as an empty container with no
255 * data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input
256 * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via
257 * {@link #addHDU(BasicHDU)} to the container.
258 * </p>
259 *
260 * @param src the random access input. The content of this input will not be read into the Fits object
261 * until the user makes some explicit request.
262 *
263 * @throws FitsException if the operation failed
264 *
265 * @see #Fits(File)
266 * @see #Fits(FitsFile)
267 * @see #read()
268 * @see #getHDU(int)
269 * @see #readHDU()
270 * @see #skipHDU()
271 * @see #addHDU(BasicHDU)
272 */
273 public Fits(RandomAccessFileIO src) throws FitsException {
274 randomInit(src);
275 }
276
277 /**
278 * <p>
279 * Creates a new (empty) FITS container associated with {@link FitsFile} input.
280 * </p>
281 * <p>
282 * While the FITS object is associated with the specified file input, it is initialized as an empty container with
283 * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input
284 * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via
285 * {@link #addHDU(BasicHDU)} to the container.
286 * </p>
287 *
288 * @param src the random access input. The content of this input will not be read into the Fits object
289 * until the user makes some explicit request.
290 *
291 * @throws FitsException if the input could not bew repositions to its beginning
292 *
293 * @see #Fits(File)
294 * @see #Fits(RandomAccessFileIO)
295 * @see #read()
296 * @see #getHDU(int)
297 * @see #readHDU()
298 * @see #skipHDU()
299 * @see #addHDU(BasicHDU)
300 *
301 * @since 1.18
302 */
303 public Fits(FitsFile src) throws FitsException {
304 dataStr = src;
305 try {
306 src.seek(0);
307 } catch (Exception e) {
308 throw new FitsException("Could not create Fits: " + e.getMessage(), e);
309 }
310 }
311
312 /**
313 * <p>
314 * Creates a new (empty) FITS container associated with the given input stream. Compression is determined from the
315 * first few bytes of the stream.
316 * </p>
317 * <p>
318 * While the FITS object is associated with the specified input stream, it is initialized as an empty container with
319 * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input
320 * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via
321 * {@link #addHDU(BasicHDU)} to the container.
322 * </p>
323 *
324 * @param str The data stream. The content of this stream will not be read into the Fits object until the
325 * user makes some explicit request.
326 *
327 * @throws FitsException if the operation failed
328 *
329 * @see #Fits(File)
330 * @see #Fits(FitsFile)
331 * @see #read()
332 * @see #getHDU(int)
333 * @see #readHDU()
334 * @see #skipHDU()
335 * @see #addHDU(BasicHDU)
336 */
337 public Fits(InputStream str) throws FitsException {
338 streamInit(str);
339 }
340
341 /**
342 * <p>
343 * Creates a new (empty) FITS container associated with an input stream.
344 * </p>
345 * <p>
346 * While the FITS object is associated with the specified input stream, it is initialized as an empty container with
347 * no data loaded from the input automatically. You may want to call {@link #read()} to load all data from the input
348 * and/or {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via
349 * {@link #addHDU(BasicHDU)} to the container.
350 * </p>
351 *
352 * @param str The data stream. The content of this stream will not be read into the Fits object until
353 * the user makes some explicit request.
354 * @param compressed Is the stream compressed? This is currently ignored. Compression is determined from the
355 * first two bytes in the stream.
356 *
357 * @throws FitsException if the operation failed
358 *
359 * @deprecated Use {@link #Fits(InputStream)} instead (compression is auto detected). Will remove in
360 * the future.
361 *
362 * @see #Fits(InputStream)
363 */
364 @Deprecated
365 public Fits(InputStream str, boolean compressed) throws FitsException {
366 this(str);
367 LOG.log(Level.INFO, "compression ignored, will be autodetected. was set to " + compressed);
368 }
369
370 /**
371 * <p>
372 * Creates a new (empty) FITS container with a file or URL as its input. The string is assumed to be a URL if it
373 * begins one of the protocol strings. If the string ends in .gz it is assumed that the data is in a compressed
374 * format. All string comparisons are case insensitive.
375 * </p>
376 * <p>
377 * While the FITS object is associated with the specified file, it is initialized as an empty container with no data
378 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
379 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
380 * the container.
381 * </p>
382 *
383 * @param filename The name of the file or URL to be processed. The content of this file will not be read into
384 * the Fits object until the user makes some explicit request.
385 *
386 * @throws FitsException Thrown if unable to find or open a file or URL from the string given.
387 *
388 * @see #Fits(URL)
389 * @see #Fits(FitsFile)
390 * @see #Fits(File)
391 * @see #read()
392 * @see #getHDU(int)
393 * @see #readHDU()
394 * @see #skipHDU()
395 * @see #addHDU(BasicHDU)
396 **/
397 public Fits(String filename) throws FitsException {
398 this(filename, CompressionManager.isCompressed(filename));
399 }
400
401 /**
402 * <p>
403 * Creates a new (empty) FITS container associated with a file or URL as its input. The string is assumed to be a
404 * URL if it begins one of the protocol strings. If the string ends in .gz it is assumed that the data is in a
405 * compressed format. All string comparisons are case insensitive.
406 * </p>
407 * <p>
408 * While the FITS object is associated with the specified file, it is initialized as an empty container with no data
409 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
410 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
411 * the container.
412 * </p>
413 *
414 * @param filename The name of the file or URL to be processed. The content of this file will not be read
415 * into the Fits object until the user makes some explicit request.
416 * @param compressed is the file compressed?
417 *
418 * @throws FitsException Thrown if unable to find or open a file or URL from the string given.
419 *
420 * @deprecated Use {@link #Fits(String)} instead (compression is auto detected). Will be a private
421 * method in the future.
422 *
423 * @see #Fits(String)
424 **/
425 @SuppressWarnings("resource")
426 @Deprecated
427 public Fits(String filename, boolean compressed) throws FitsException {
428 if (filename == null) {
429 throw new FitsException("Null FITS Identifier String");
430 }
431 try {
432 File fil = new File(filename);
433 if (fil.exists()) {
434 fileInit(fil, compressed);
435 return;
436 }
437 } catch (Exception e) {
438 LOG.log(Level.FINE, "not a file " + filename, e);
439 throw new FitsException("could not detect type of " + filename, e);
440 }
441 try {
442 InputStream str = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
443 if (str != null) {
444 streamInit(str);
445 return;
446 }
447 } catch (Exception e) {
448 LOG.log(Level.FINE, "not a resource " + filename, e);
449 throw new FitsException("could not detect type of " + filename, e);
450 }
451 try {
452 InputStream is = FitsUtil.getURLStream(new URL(filename), 0);
453 streamInit(is);
454 return;
455 } catch (Exception e) {
456 LOG.log(Level.FINE, "not a url " + filename, e);
457 throw new FitsException("could not detect type of " + filename, e);
458 }
459
460 }
461
462 /**
463 * <p>
464 * Creates a new (empty) FITS container with a given URL as its input.
465 * </p>
466 * <p>
467 * While the FITS object is associated with the resource, it is initialized as an empty container with no data
468 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
469 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
470 * the container.
471 * </p>
472 *
473 * @param myURL The URL to be read. The content of this URL will not be read into the Fits object until the
474 * user makes some explicit request.
475 *
476 * @throws FitsException Thrown if unable to find or open a file or URL from the string given.
477 *
478 * @see #Fits(String)
479 * @see #Fits(RandomAccessFileIO)
480 * @see #read()
481 * @see #getHDU(int)
482 * @see #readHDU()
483 * @see #skipHDU()
484 * @see #addHDU(BasicHDU)
485 */
486 @SuppressWarnings("resource")
487 public Fits(URL myURL) throws FitsException {
488 try {
489 streamInit(FitsUtil.getURLStream(myURL, 0));
490 } catch (IOException e) {
491 throw new FitsException("Unable to open input from URL:" + myURL, e);
492 }
493 }
494
495 /**
496 * <p>
497 * Creates a new (empty) FITS container associated with a given uncompressed URL as its input.
498 * </p>
499 * <p>
500 * While the FITS object is associated with the resource, it is initialized as an empty container with no data
501 * loaded from the input automatically. You may want to call {@link #read()} to load all data from the input and/or
502 * {@link #readHDU()}/{@link #getHDU(int)} for select HDUs, which you can then add via {@link #addHDU(BasicHDU)} to
503 * the container.
504 * </p>
505 *
506 * @param myURL The URL to be associated with the FITS file. The content of this URL will not be read
507 * into the Fits object until the user makes some explicit request.
508 * @param compressed Compression flag, ignored.
509 *
510 * @throws FitsException Thrown if unable to use the specified URL.
511 *
512 * @deprecated Use {@link #Fits(URL)} instead (compression is auto detected). Will remove in the
513 * future.
514 *
515 * @see #Fits(URL)
516 */
517 @Deprecated
518 public Fits(URL myURL, boolean compressed) throws FitsException {
519 this(myURL);
520 LOG.log(Level.INFO, "compression ignored, will be autodetected. was set to " + compressed);
521 }
522
523 /**
524 * Creates a new empty HDU for the given data type.
525 *
526 * @return a newly created HDU from the given Data.
527 *
528 * @param data The data to be described in this HDU.
529 * @param <DataClass> the class of the HDU
530 *
531 * @throws FitsException if the operation failed
532 */
533 public static <DataClass extends Data> BasicHDU<DataClass> makeHDU(DataClass data) throws FitsException {
534 Header hdr = new Header();
535 data.fillHeader(hdr);
536 return FitsFactory.hduFactory(hdr, data);
537 }
538
539 /**
540 * Creates a new empty HDU based on the header description of the data
541 *
542 * @return a newly created HDU from the given header (and including the header).
543 *
544 * @param h The header which describes the FITS extension
545 *
546 * @throws FitsException if the header could not be converted to a HDU.
547 */
548 public static BasicHDU<?> makeHDU(Header h) throws FitsException {
549 Data d = FitsFactory.dataFactory(h);
550 return FitsFactory.hduFactory(h, d);
551 }
552
553 /**
554 * <p>
555 * Creates an HDU that wraps around the specified data object. The HDUs header will be created and populated with
556 * the essential description of the data. The following HDU types may be returned depending on the nature of the
557 * argument:
558 * </p>
559 * <ul>
560 * <li>{@link NullDataHDU} -- if the argument is <code>null</code></li>
561 * <li>{@link ImageHDU} -- if the argument is a regular numerical array, such as a <code>double[]</code>,
562 * <code>float[][]</code>, or <code>short[][][]</code></li>
563 * <li>{@link BinaryTableHDU} -- the the argument is an <code>Object[rows][cols]</code> type array with a regular
564 * structure and supported column data types, provided that it cannot be represented by an ASCII table <b>OR</b> if
565 * {@link FitsFactory#getUseAsciiTables()} is <code>false</code></li>
566 * <li>{@link AsciiTableHDU} -- Like above, but only when the data can be represented by an ASCII table <b>AND</b>
567 * {@link FitsFactory#getUseAsciiTables()} is <code>true</code></li>
568 * </ul>
569 * <p>
570 * As of 1.18, this metohd will not create and return random group HDUs for <code>Object[][2]</code> style data.
571 * Instead, it will return an appropriate binary or ASCII table, since the FITS standard recommends against using
572 * random groups going forward, except for reading some old data from certain radio telescopes. If the need ever
573 * arises to create new random groups HDUs with this library, you may use
574 * {@link RandomGroupsHDU#createFrom(Object[][])} instead.
575 * </p>
576 *
577 * @return a newly created HDU from the given data kernel.
578 *
579 * @param o The data to be described in this HDU.
580 *
581 * @throws FitsException if the parameter could not be converted to a HDU.
582 *
583 * @see RandomGroupsHDU#createFrom(Object[][])
584 */
585 public static BasicHDU<?> makeHDU(Object o) throws FitsException {
586 return FitsFactory.hduFactory(o);
587 }
588
589 /**
590 * Returns the version sting of this FITS library
591 *
592 * @return the version of the library.
593 */
594 public static String version() {
595 Properties props = new Properties();
596 try (InputStream versionProperties = Fits.class
597 .getResourceAsStream("/META-INF/maven/gov.nasa.gsfc.heasarc/nom-tam-fits/pom.properties")) {
598 props.load(versionProperties);
599 return props.getProperty("version");
600 } catch (IOException e) {
601 LOG.log(Level.INFO, "reading version failed, ignoring", e);
602 return "unknown";
603 }
604 }
605
606 /**
607 * close the input stream, and ignore eventual errors.
608 *
609 * @deprecated Use <b>try-with-resources</b> constructs in Java 8+ instead.
610 *
611 * @param in the input stream to close.
612 */
613 @Deprecated
614 public static void saveClose(InputStream in) {
615 SafeClose.close(in);
616 }
617
618 /**
619 * Add an HDU to the Fits object. Users may intermix calls to functions which read HDUs from an associated input
620 * stream with the addHDU and insertHDU calls, but should be careful to understand the consequences.
621 *
622 * @param myHDU The HDU to be added to the end of the FITS object.
623 *
624 * @throws FitsException if the HDU could not be inserted.
625 *
626 * @see #readHDU()
627 */
628 public void addHDU(BasicHDU<?> myHDU) throws FitsException {
629 insertHDU(myHDU, getNumberOfHDUs());
630 }
631
632 /**
633 * Get the current number of HDUs in the Fits object.
634 *
635 * @return The number of HDU's in the object.
636 *
637 * @deprecated use {@link #getNumberOfHDUs()} instead
638 */
639 @Deprecated
640 public int currentSize() {
641 return getNumberOfHDUs();
642 }
643
644 /**
645 * Delete an HDU from the HDU list.
646 *
647 * @param n The index of the HDU to be deleted. If n is 0 and there is more than one HDU present, then
648 * the next HDU will be converted from an image to primary HDU if possible. If not a dummy
649 * header HDU will then be inserted.
650 *
651 * @throws FitsException if the HDU could not be deleted.
652 */
653 public void deleteHDU(int n) throws FitsException {
654 int size = getNumberOfHDUs();
655 if (n < 0 || n >= size) {
656 throw new FitsException("Attempt to delete non-existent HDU:" + n);
657 }
658 hduList.remove(n);
659 if (n == 0 && size > 1) {
660 BasicHDU<?> newFirst = hduList.get(0);
661 if (newFirst.canBePrimary()) {
662 newFirst.setPrimaryHDU(true);
663 } else {
664 insertHDU(BasicHDU.getDummyHDU(), 0);
665 }
666 }
667 }
668
669 /**
670 * @deprecated Will be private in 2.0. Get a stream from the file and then use the stream
671 * initialization.
672 *
673 * @param myFile The File to be associated.
674 * @param compressed Is the data compressed?
675 *
676 * @throws FitsException if the opening of the file failed.
677 */
678 // TODO Make private
679 @Deprecated
680 @SuppressWarnings("resource")
681 protected void fileInit(File myFile, boolean compressed) throws FitsException {
682 try {
683 if (compressed) {
684 streamInit(Files.newInputStream(myFile.toPath()));
685 } else {
686 randomInit(myFile);
687 }
688 } catch (IOException e) {
689 throw new FitsException("Unable to create Input Stream from File: " + myFile, e);
690 }
691 }
692
693 /**
694 * Returns the n'th HDU. If the HDU is already read simply return a pointer to the cached data. Otherwise read the
695 * associated stream until the n'th HDU is read.
696 *
697 * @param n The index of the HDU to be read. The primary HDU is index 0.
698 *
699 * @return The n'th HDU or null if it could not be found.
700 *
701 * @throws FitsException if the header could not be read
702 * @throws IOException if the underlying buffer threw an error
703 * @throws IndexOutOfBoundsException if the Fits contains no HDU by the given index.
704 *
705 * @see #getHDU(String)
706 * @see #getHDU(String, int)
707 */
708 public BasicHDU<?> getHDU(int n) throws FitsException, IOException, IndexOutOfBoundsException {
709 for (int i = getNumberOfHDUs(); i <= n; i++) {
710 BasicHDU<?> hdu = readHDU();
711 if (hdu == null) {
712 return null;
713 }
714 }
715 return hduList.get(n);
716 }
717
718 /**
719 * Returns the primary header of this FITS file, that is the header of the primary HDU in this Fits object. This
720 * method differs from <code>getHDU(0).getHeader()</code>, int that the primary header this way will be properly
721 * configured as the primary HDU with all mandatory keywords, even if the HDU's header did not contain these entries
722 * originally. (Subsequent calls to <code>getHDU(0).getHeader()</code> will also contain the populated mandatory
723 * keywords).
724 *
725 * @return The primary header of this FITS file/object.
726 *
727 * @throws FitsException If the Fits is empty (does not contain a primary HDU)
728 * @throws IOException if there was a problem accessing the FITS from the input
729 *
730 * @see #getCompleteHeader(int)
731 * @see BasicHDU#getHeader()
732 *
733 * @since 1.19
734 */
735 public Header getPrimaryHeader() throws FitsException, IOException {
736 if (hduList.isEmpty()) {
737 throw new FitsException("Empty Fits object");
738 }
739 BasicHDU<?> primary = getHDU(0);
740 primary.setPrimaryHDU(true);
741 return primary.getHeader();
742 }
743
744 /**
745 * Returns the 'complete' header of the n<sup>th</sup> HDU in this FITS file/object. This differs from
746 * {@link #getHDU(int)}<code>.getHeader()</code> in two important ways:
747 * <ul>
748 * <li>The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or
749 * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to <code>getHDU(n).getHeader()</code>
750 * will also include the populated mandatory keywords.)</li>
751 * <li>If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges
752 * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already.
753 * </ul>
754 *
755 * @param n The zero-based index of the HDU.
756 *
757 * @return The completed header of the HDU. If the HDU contains the INHERIT key this
758 * header will be a new header object constructed by this call to include also
759 * all non-conflicting primary header keywords. Otherwise it will simply
760 * return the HDUs header (after adding the mandatory keywords).
761 *
762 * @throws FitsException If the FITS is empty
763 * @throws IOException If the HDU is not accessible from its source
764 * @throws IndexOutOfBoundsException If the FITS does not contain a HDU by the specified index
765 *
766 * @see #getCompleteHeader(String)
767 * @see #getCompleteHeader(String, int)
768 * @see #getPrimaryHeader()
769 * @see #getHDU(int)
770 *
771 * @since 1.19
772 */
773 public Header getCompleteHeader(int n) throws FitsException, IOException, IndexOutOfBoundsException {
774 BasicHDU<?> hdu = getHDU(n);
775 if (hdu == null) {
776 throw new IndexOutOfBoundsException("FITS has no HDU index " + n);
777 }
778 return getCompleteHeader(hdu);
779 }
780
781 /**
782 * Returns the complete header of the first HDU by the specified name in this FITS file/object. This differs from
783 * {@link #getHDU(String)}<code>.getHeader()</code> in two important ways:
784 * <ul>
785 * <li>The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or
786 * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to <code>getHDU(n).getHeader()</code>
787 * will also include the populated mandatory keywords.)</li>
788 * <li>If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges
789 * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already.
790 * </ul>
791 *
792 * @param name The HDU name
793 *
794 * @return The completed header of the HDU. If the HDU contains the INHERIT key this header
795 * will be a new header object constructed by this call to include also all
796 * non-conflicting primary header keywords. Otherwise it will simply return the
797 * HDUs header (after adding the mandatory keywords).
798 *
799 * @throws FitsException If the FITS is empty
800 * @throws IOException If the HDU is not accessible from its source
801 * @throws NoSuchElementException If the FITS does not contain a HDU by the specified name
802 *
803 * @see #getCompleteHeader(String, int)
804 * @see #getCompleteHeader(int)
805 * @see #getPrimaryHeader()
806 * @see #getHDU(int)
807 *
808 * @since 1.19
809 */
810 public Header getCompleteHeader(String name) throws FitsException, IOException, NoSuchElementException {
811 BasicHDU<?> hdu = getHDU(name);
812 if (hdu == null) {
813 throw new NoSuchElementException("Fits contains no HDU named " + name);
814 }
815 return getCompleteHeader(hdu);
816 }
817
818 /**
819 * Returns the complete header of the first HDU by the specified name and version in this FITS file/object. This
820 * differs from {@link #getHDU(String)}<code>.getHeader()</code> in two important ways:
821 * <ul>
822 * <li>The header will be populated with the mandatory FITS keywords based on whether it is that of a primary or
823 * extension HDU in this Fits, and the type of HDU it is. (Subsequent calls to <code>getHDU(n).getHeader()</code>
824 * will also include the populated mandatory keywords.)</li>
825 * <li>If the header contains the {@link Standard#INHERIT} keyword, a new header object is returned, which merges
826 * the non-conflicting primary header keys on top of the keywords explicitly defined in the HDU already.
827 * </ul>
828 *
829 * @param name The HDU name
830 * @param version The HDU version
831 *
832 * @return The completed header of the HDU. If the HDU contains the INHERIT key this header
833 * will be a new header object constructed by this call to include also all
834 * non-conflicting primary header keywords. Otherwise it will simply return the
835 * HDUs header (after adding the mandatory keywords).
836 *
837 * @throws FitsException If the FITS is empty
838 * @throws IOException If the HDU is not accessible from its source
839 * @throws NoSuchElementException If the FITS does not contain a HDU by the specified name and version
840 *
841 * @see #getCompleteHeader(String)
842 * @see #getCompleteHeader(int)
843 * @see #getPrimaryHeader()
844 * @see #getHDU(int)
845 *
846 * @since 1.19
847 */
848 public Header getCompleteHeader(String name, int version) throws FitsException, IOException, NoSuchElementException {
849 BasicHDU<?> hdu = getHDU(name, version);
850 if (hdu == null) {
851 throw new NoSuchElementException("Fits contains no HDU named " + name);
852 }
853 return getCompleteHeader(hdu);
854 }
855
856 private Header getCompleteHeader(BasicHDU<?> hdu) throws FitsException, IOException {
857 if (hdu == getHDU(0)) {
858 return getPrimaryHeader();
859 }
860 hdu.setPrimaryHDU(false);
861 Header h = hdu.getHeader();
862 if (h.getBooleanValue(Standard.INHERIT)) {
863 Header merged = new Header();
864 merged.mergeDistinct(h);
865 merged.mergeDistinct(getPrimaryHeader());
866 return merged;
867 }
868 return h;
869 }
870
871 /**
872 * Checks if the value of the EXTNAME keyword of the specified HDU matches the specified name.
873 *
874 * @param hdu The HDU whose EXTNAME to check
875 * @param name The expected name
876 *
877 * @return <code>true</code> if the HDU has an EXTNAME keyword whose value matches the specified name (case
878 * sensitive!), otherwise <code>false</code>
879 *
880 * @see #getHDU(String)
881 */
882 private boolean isNameMatch(BasicHDU<?> hdu, String name) {
883 Header h = hdu.getHeader();
884 if (!h.containsKey(EXTNAME)) {
885 return false;
886 }
887 return name.equals(hdu.getHeader().getStringValue(EXTNAME));
888 }
889
890 /**
891 * Checks if the value of the EXTNAME and EXTVER keywords of the specified HDU match the specified name and version.
892 *
893 * @param hdu The HDU whose EXTNAME to check
894 * @param name The expected name
895 * @param version The expected extension version
896 *
897 * @return <code>true</code> if the HDU has an EXTNAME keyword whose value matches the specified name (case
898 * sensitive!) AND has an EXTVER keyword whose value matches the specified integer version. In
899 * all other cases <code>false</code> is returned.
900 *
901 * @see #getHDU(String, int)
902 */
903 private boolean isNameVersionMatch(BasicHDU<?> hdu, String name, int version) {
904 Header h = hdu.getHeader();
905 if (!h.containsKey(EXTNAME) || !name.equals(h.getStringValue(EXTNAME)) || !h.containsKey(EXTVER)) {
906 return false;
907 }
908 return h.getIntValue(EXTVER) == version;
909 }
910
911 /**
912 * Returns the HDU by the given extension name (defined by <code>EXTNAME</code> header keyword). This method checks
913 * only for EXTNAME but will ignore the version (defined by <code>EXTVER</code>). If multiple HDUs have the same
914 * matching <code>EXTNAME</code>, this method will return the first match only.
915 *
916 * @param name The name of the HDU as defined by <code>EXTNAME</code> (case sensitive)
917 *
918 * @return The first HDU that matches the specified extension name and version, or <code>null</code>
919 * if the FITS does not contain a matching HDU.
920 *
921 * @throws FitsException if the header could not be read
922 * @throws IOException if the underlying buffer threw an error
923 *
924 * @since 1.17.0
925 *
926 * @see #getHDU(String, int)
927 * @see #getHDU(int)
928 */
929 public BasicHDU<?> getHDU(String name) throws FitsException, IOException {
930 // Check HDUs we already read...
931 for (BasicHDU<?> hdu : hduList) {
932 if (isNameMatch(hdu, name)) {
933 return hdu;
934 }
935 }
936
937 // Read additional HDUs as necessary...
938 BasicHDU<?> hdu;
939 while ((hdu = readHDU()) != null) {
940 if (isNameMatch(hdu, name)) {
941 return hdu;
942 }
943 }
944
945 return null;
946 }
947
948 /**
949 * Returns the HDU by the given extension name and version (defined by <code>EXTNAME</code> and <code>EXTVER</code>
950 * keywords). If multiple HDUs have the same matching name and version, this method will return the first match
951 * only.
952 *
953 * @param name The name of the HDU as defined by <code>EXTNAME</code> (case sensitive)
954 * @param version The extension version as defined by <code>EXTVER</code> in the matching HDU.
955 *
956 * @return The first HDU that matches the specified extension name and version, or <code>null</code>
957 * if the FITS does not contain a matching HDU.
958 *
959 * @throws FitsException if the header could not be read
960 * @throws IOException if the underlying buffer threw an error
961 *
962 * @since 1.17.0
963 *
964 * @see #getHDU(String)
965 * @see #getHDU(int)
966 */
967 public BasicHDU<?> getHDU(String name, int version) throws FitsException, IOException {
968 // Check HDUs we already read...
969 for (BasicHDU<?> hdu : hduList) {
970 if (isNameVersionMatch(hdu, name, version)) {
971 return hdu;
972 }
973 }
974
975 // Read additional HDUs as necessary...
976 BasicHDU<?> hdu;
977 while ((hdu = readHDU()) != null) {
978 if (isNameVersionMatch(hdu, name, version)) {
979 return hdu;
980 }
981 }
982
983 return null;
984 }
985
986 /**
987 * Get the number of HDUs currently available in memory. For FITS objects associated with an input this method
988 * returns only the number of HDUs that have already been read / scanned, e.g. via {@link #readHDU()} or
989 * {@link #read()} methods. Thus, if you want to know how many HDUs a FITS file might actually contain, you should
990 * call {@link #read()} to register them all before calling this method.
991 *
992 * @return The number of HDU's in the object.
993 *
994 * @see #read()
995 * @see #readHDU()
996 */
997 public int getNumberOfHDUs() {
998 return hduList.size();
999 }
1000
1001 /**
1002 * Returns the input from which this <code>Fits</code> is associated to (if any)..
1003 *
1004 * @return The associated data input, or <code>null</code> if this <code>Fits</code> container was not read from an
1005 * input. Users may wish to call this function after opening a Fits object when they want low-level
1006 * rea/wrte access to the FITS resource directly.
1007 */
1008 public ArrayDataInput getStream() {
1009 return dataStr;
1010 }
1011
1012 /**
1013 * Insert a FITS object into the list of HDUs.
1014 *
1015 * @param myHDU The HDU to be inserted into the list of HDUs.
1016 * @param position The location at which the HDU is to be inserted.
1017 *
1018 * @throws FitsException if the HDU could not be inserted.
1019 */
1020 public void insertHDU(BasicHDU<?> myHDU, int position) throws FitsException {
1021 if (myHDU == null) {
1022 return;
1023 }
1024 if (position < 0 || position > getNumberOfHDUs()) {
1025 throw new FitsException("Attempt to insert HDU at invalid location: " + position);
1026 }
1027 if (myHDU instanceof RandomGroupsHDU && position != 0) {
1028 throw new FitsException("Random groups HDUs must be the first (primary) HDU. Requested pos: " + position);
1029 }
1030
1031 try {
1032 if (position == 0) {
1033 // Note that the previous initial HDU is no longer the first.
1034 // If we were to insert tables backwards from last to first,
1035 // we could get a lot of extraneous DummyHDUs but we currently
1036 // do not worry about that.
1037 if (getNumberOfHDUs() > 0) {
1038 hduList.get(0).setPrimaryHDU(false);
1039 }
1040 if (myHDU.canBePrimary()) {
1041 myHDU.setPrimaryHDU(true);
1042 hduList.add(0, myHDU);
1043 } else {
1044 insertHDU(BasicHDU.getDummyHDU(), 0);
1045 myHDU.setPrimaryHDU(false);
1046 hduList.add(1, myHDU);
1047 }
1048 } else {
1049 myHDU.setPrimaryHDU(false);
1050 hduList.add(position, myHDU);
1051 }
1052 } catch (NoSuchElementException e) {
1053 throw new FitsException("hduList inconsistency in insertHDU", e);
1054 }
1055 }
1056
1057 /**
1058 * Initialize using buffered random access. This implies that the data is uncompressed.
1059 *
1060 * @param file the file to open
1061 *
1062 * @throws FitsException if the file could not be read
1063 *
1064 * @see #randomInit(RandomAccessFileIO)
1065 */
1066 // TODO make private
1067 @Deprecated
1068 protected void randomInit(File file) throws FitsException {
1069
1070 if (!file.exists() || !file.canRead()) {
1071 throw new FitsException("Non-existent or unreadable file");
1072 }
1073 try {
1074 // Attempt to open the file for reading and writing.
1075 dataStr = new FitsFile(file, "rw");
1076 ((FitsFile) dataStr).seek(0);
1077 } catch (IOException e) {
1078 try {
1079 // If that fails, try read-only.
1080 dataStr = new FitsFile(file, "r");
1081 ((FitsFile) dataStr).seek(0);
1082 } catch (IOException e2) {
1083 throw new FitsException("Unable to open file " + file.getPath(), e2);
1084 }
1085 }
1086 }
1087
1088 /**
1089 * Initialize using buffered random access. This implies that the data is uncompressed.
1090 *
1091 * @param src the random access data
1092 *
1093 * @throws FitsException ` if the data is not readable
1094 *
1095 * @see #randomInit(File)
1096 */
1097 protected void randomInit(RandomAccessFileIO src) throws FitsException {
1098 try {
1099 dataStr = new FitsFile(src, FitsIO.DEFAULT_BUFFER_SIZE);
1100 ((FitsFile) dataStr).seek(0);
1101 } catch (IOException e) {
1102 throw new FitsException("Unable to open data " + src, e);
1103 }
1104 }
1105
1106 /**
1107 * Return all HDUs for the Fits object. If the FITS file is associated with an external stream make sure that we
1108 * have exhausted the stream.
1109 *
1110 * @return an array of all HDUs in the Fits object. Returns null if there are no HDUs associated with
1111 * this object.
1112 *
1113 * @throws FitsException if the reading failed.
1114 */
1115 public BasicHDU<?>[] read() throws FitsException {
1116 readToEnd();
1117 int size = getNumberOfHDUs();
1118 if (size == 0) {
1119 return new BasicHDU<?>[0];
1120 }
1121 return hduList.toArray(new BasicHDU<?>[size]);
1122 }
1123
1124 /**
1125 * Read a FITS file from an InputStream object.
1126 *
1127 * @param is The InputStream stream whence the FITS information is found.
1128 *
1129 * @throws FitsException if the data read could not be interpreted
1130 *
1131 * @deprecated Use {@link #Fits(InputStream)} constructor instead. We will remove this method in the
1132 * future.
1133 */
1134 @Deprecated
1135 public void read(InputStream is) throws FitsException {
1136 is = CompressionManager.decompress(is);
1137
1138 if (is instanceof ArrayDataInput) {
1139 dataStr = (ArrayDataInput) is;
1140 } else {
1141 dataStr = new FitsInputStream(is);
1142 }
1143 read();
1144 }
1145
1146 /**
1147 * Read the next HDU on the default input stream. This call may return any concrete subclass of {@link BasicHDU},
1148 * including compressed HDU types.
1149 *
1150 * @return The HDU read, or null if an EOF was detected. Note that null is only returned when the EOF
1151 * is detected immediately at the beginning of reading the HDU.
1152 *
1153 * @throws FitsException if the header could not be read
1154 * @throws IOException if the underlying buffer threw an error
1155 *
1156 * @see #skipHDU()
1157 * @see #getHDU(int)
1158 * @see #addHDU(BasicHDU)
1159 */
1160 public BasicHDU<?> readHDU() throws FitsException, IOException {
1161 if (dataStr == null || atEOF) {
1162 if (dataStr == null) {
1163 LOG.warning("trying to read a hdu, without an input source!");
1164 }
1165 return null;
1166 }
1167
1168 if (dataStr instanceof RandomAccess && lastFileOffset > 0) {
1169 FitsUtil.reposition(dataStr, lastFileOffset);
1170 }
1171
1172 Header hdr = Header.readHeader(dataStr);
1173 if (hdr == null) {
1174 atEOF = true;
1175 return null;
1176 }
1177
1178 Data data = FitsFactory.dataFactory(hdr);
1179 try {
1180 data.read(dataStr);
1181 if (Fits.checkTruncated(dataStr)) {
1182 // Check for truncation even if we successfully skipped to the expected
1183 // end since skip may allow going beyond the EOF.
1184 LOG.warning("Missing padding after data segment");
1185 }
1186 } catch (PaddingException e) {
1187 // Stream end before required padding after data...
1188 LOG.warning(e.getMessage());
1189 }
1190
1191 lastFileOffset = FitsUtil.findOffset(dataStr);
1192 BasicHDU<Data> hdu = FitsFactory.hduFactory(hdr, data);
1193
1194 hduList.add(hdu);
1195
1196 return hdu;
1197 }
1198
1199 /**
1200 * Read to the end of the associated input stream
1201 *
1202 * @throws FitsException if the operation failed
1203 */
1204 private void readToEnd() throws FitsException {
1205 try {
1206 while (dataStr != null && !atEOF) {
1207 if (readHDU() == null) {
1208 if (getNumberOfHDUs() == 0) {
1209 throw new FitsException("Not FITS file.");
1210 }
1211 return;
1212 }
1213 }
1214 } catch (IOException e) {
1215 throw new FitsException("Corrupted FITS file: " + e, e);
1216 }
1217 }
1218
1219 /**
1220 * <p>
1221 * Computes the <code>CHECKSUM</code> and <code>DATASUM</code> values for the specified HDU index and stores them in
1222 * the HUS's header. For deferred data the data sum is calculated directly from the file (if possible), without
1223 * loading the entire (potentially huge) data into RAM for the calculation.
1224 * </p>
1225 *
1226 * @param hduIndex The index of the HDU for which to compute and set the <code>CHECKSUM</code> and
1227 * <code>DATASUM</code> header values.
1228 *
1229 * @throws FitsException if there was a problem computing the checksum for the HDU
1230 * @throws IOException if there was an I/O error while accessing the data from the input
1231 *
1232 * @see #setChecksum()
1233 * @see BasicHDU#verifyIntegrity()
1234 * @see BasicHDU#verifyDataIntegrity()
1235 *
1236 * @since 1.17
1237 */
1238 public void setChecksum(int hduIndex) throws FitsException, IOException {
1239 FitsCheckSum.setDatasum(getHDU(hduIndex).getHeader(), calcDatasum(hduIndex));
1240 }
1241
1242 /**
1243 * <p>
1244 * Add or modify the CHECKSUM keyword in all headers. As of 1.17 the checksum for deferred data is calculated
1245 * directly from the file (if possible), without loading the entire (potentially huge) data into RAM for the
1246 * calculation.
1247 * </p>
1248 * <p>
1249 * As of 1.17, the routine calculates checksums both for HDUs that are in RAM, as well as HDUs that were not yet
1250 * loaded from the input (if any). Any HDUs not in RAM at the time of the call will stay in deferred mode (if the
1251 * HDU itself supports it). After setting (new) checksums, you may want to call #rewrite()
1252 * </p>
1253 *
1254 * @throws FitsException if there was an error during the checksumming operation
1255 * @throws IOException if there was an I/O error while accessing the data from the input
1256 *
1257 * @see #setChecksum(int)
1258 * @see BasicHDU#getStoredDatasum()
1259 * @see #rewrite()
1260 */
1261 public void setChecksum() throws FitsException, IOException {
1262 int i = 0;
1263
1264 // Start with HDU's already loaded, leaving deferred data in unloaded
1265 // state
1266 for (; i < getNumberOfHDUs(); i++) {
1267 setChecksum(i);
1268 }
1269
1270 // Check if Fits is read from an input of sorts, with potentially more
1271 // HDUs there...
1272 if (dataStr == null) {
1273 return;
1274 }
1275
1276 // Continue with unread HDUs (if any...)
1277 while (readHDU() != null) {
1278 setChecksum(i++);
1279 }
1280 }
1281
1282 /**
1283 * <p>
1284 * Calculates the data checksum for a given HDU in the Fits. If the HDU does not currently have data loaded from
1285 * disk (in deferred read mode), the method will calculate the checksum directly from disk. Otherwise, it will
1286 * calculate the datasum from the data in memory.
1287 * </p>
1288 *
1289 * @param hduIndex The index of the HDU for which to calculate the data checksum
1290 *
1291 * @return The data checksum. This may differ from the datasum or the original FITS input due to
1292 * differences in padding used at the end of the data record by this library vs the
1293 * library that was used to generate the FITS.
1294 *
1295 * @throws FitsException if there was an error processing the HDU.
1296 * @throws IOException if there was an I/O error accessing the input.
1297 *
1298 * @see Data#calcChecksum()
1299 * @see BasicHDU#verifyDataIntegrity()
1300 * @see #setChecksum(int)
1301 * @see BasicHDU#getStoredDatasum()
1302 * @see FitsCheckSum#setDatasum(Header, long)
1303 *
1304 * @since 1.17
1305 */
1306 public long calcDatasum(int hduIndex) throws FitsException, IOException {
1307 BasicHDU<?> hdu = getHDU(hduIndex);
1308 Data data = hdu.getData();
1309 if (data.isDeferred()) {
1310 // Compute datasum directly from file...
1311 return FitsCheckSum.checksum((RandomAccess) dataStr, data.getFileOffset(), data.getSize());
1312 }
1313 return data.calcChecksum();
1314 }
1315
1316 /**
1317 * Calculates the FITS checksum for a given HDU in the Fits. If the HDU does not currently have data loaded from
1318 * disk (i.e. in deferred read mode), the method will compute the checksum directly from disk. Otherwise, it will
1319 * calculate the checksum from the data in memory and using the standard padding after it.
1320 *
1321 * @deprecated Use {@link BasicHDU#verifyIntegrity()} instead when appropriate. It's not particularly
1322 * useful since integrity checking does not use or require knowledge of this sum. May
1323 * be removed from future releases.
1324 *
1325 * @param hduIndex The index of the HDU for which to calculate the HDU checksum
1326 *
1327 * @return The checksum value that would appear in the header if this HDU was written to an
1328 * output. This may differ from the checksum recorded in the input, due to different
1329 * formating conventions used by this library vs the one that was used to generate the
1330 * input.
1331 *
1332 * @throws FitsException if there was an error processing the HDU.
1333 * @throws IOException if there was an I/O error accessing the input.
1334 *
1335 * @see BasicHDU#calcChecksum()
1336 * @see #calcDatasum(int)
1337 * @see #setChecksum(int)
1338 *
1339 * @since 1.17
1340 */
1341 @Deprecated
1342 public long calcChecksum(int hduIndex) throws FitsException, IOException {
1343 return FitsCheckSum.sumOf(FitsCheckSum.checksum(getHDU(hduIndex).getHeader()), calcDatasum(hduIndex));
1344 }
1345
1346 /**
1347 * Checks the integrity of all HDUs. HDUs that do not specify either CHECKSUM or DATASUM keyword will be ignored.
1348 *
1349 * @throws FitsIntegrityException if the FITS is corrupted, the message will inform about which HDU failed the
1350 * integrity test first.
1351 * @throws FitsException if the header or HDU is invalid or garbled.
1352 * @throws IOException if the Fits object is not associated to a random-accessible input, or if there was
1353 * an I/O error accessing the input.
1354 *
1355 * @see BasicHDU#verifyIntegrity()
1356 *
1357 * @since 1.18.1
1358 */
1359 public void verifyIntegrity() throws FitsIntegrityException, FitsException, IOException {
1360 for (int i = 0;; i++) {
1361 BasicHDU<?> hdu = readHDU();
1362 if (hdu == null) {
1363 break;
1364 }
1365
1366 try {
1367 hdu.verifyIntegrity();
1368 } catch (FitsIntegrityException e) {
1369 throw new FitsIntegrityException(i, e);
1370 }
1371 }
1372 }
1373
1374 /**
1375 * @deprecated This method is poorly conceived as we cannot really read FITS from just any
1376 * <code>ArrayDataInput</code> but only those, which utilize {@link nom.tam.util.FitsDecoder}
1377 * to convert Java types to FITS binary format, such as {@link FitsInputStream} or
1378 * {@link FitsFile} (or else a wrapped <code>DataInputStream</code>). As such, this method is
1379 * inherently unsafe as it can be used to parse FITS content iscorrectly. It will be removed
1380 * from the public API in a future major release. Set the data stream to be used for future
1381 * input.
1382 *
1383 * @param stream The data stream to be used.
1384 */
1385 @Deprecated
1386 public void setStream(ArrayDataInput stream) {
1387 dataStr = stream;
1388 atEOF = false;
1389 lastFileOffset = -1;
1390 }
1391
1392 /**
1393 * Return the number of HDUs in the Fits object. If the FITS file is associated with an external stream make sure
1394 * that we have exhausted the stream.
1395 *
1396 * @return number of HDUs.
1397 *
1398 * @deprecated The meaning of size of ambiguous. Use {@link #getNumberOfHDUs()} instead. Note size()
1399 * will read the input file/stream to the EOF before returning the number of HDUs
1400 * which {@link #getNumberOfHDUs()} does not. If you wish to duplicate this behavior
1401 * and ensure that the input has been exhausted before getting the number of HDUs then
1402 * use the sequence: <code>
1403 * read();
1404 * getNumberOfHDUs();
1405 * </code>
1406 *
1407 * @throws FitsException if the file could not be read.
1408 */
1409 @Deprecated
1410 public int size() throws FitsException {
1411 readToEnd();
1412 return getNumberOfHDUs();
1413 }
1414
1415 /**
1416 * Skip the next HDU on the default input stream.
1417 *
1418 * @throws FitsException if the HDU could not be skipped
1419 * @throws IOException if the underlying stream failed
1420 *
1421 * @see #skipHDU(int)
1422 * @see #readHDU()
1423 */
1424 public void skipHDU() throws FitsException, IOException {
1425 if (atEOF) {
1426 return;
1427 }
1428
1429 Header hdr = new Header(dataStr);
1430 int dataSize = (int) hdr.getDataSize();
1431 dataStr.skipAllBytes(dataSize);
1432 if (dataStr instanceof RandomAccess) {
1433 lastFileOffset = ((RandomAccess) dataStr).getFilePointer();
1434 }
1435 }
1436
1437 /**
1438 * Skip HDUs on the associate input stream.
1439 *
1440 * @param n The number of HDUs to be skipped.
1441 *
1442 * @throws FitsException if the HDU could not be skipped
1443 * @throws IOException if the underlying stream failed
1444 *
1445 * @see #skipHDU()
1446 */
1447 public void skipHDU(int n) throws FitsException, IOException {
1448 for (int i = 0; i < n; i++) {
1449 skipHDU();
1450 }
1451 }
1452
1453 /**
1454 * Initializes the input stream. Mostly this checks to see if the stream is compressed and wraps the stream if
1455 * necessary. Even if the stream is not compressed, it will likely be wrapped in a PushbackInputStream. So users
1456 * should probably not supply a BufferedDataInputStream themselves, but should allow the Fits class to do the
1457 * wrapping.
1458 *
1459 * @param inputStream stream to initialize
1460 *
1461 * @throws FitsException if the initialization failed
1462 */
1463 protected void streamInit(InputStream inputStream) throws FitsException {
1464 dataStr = new FitsInputStream(CompressionManager.decompress(inputStream));
1465 }
1466
1467 /**
1468 * Writes the contents to a designated FITS file. It is up to the caller to close the file as appropriate after
1469 * writing to it.
1470 *
1471 * @param file a file that support FITS encoding
1472 *
1473 * @throws FitsException if there were any errors writing the contents themselves.
1474 * @throws IOException if the underlying file could not be trimmed or closed.
1475 *
1476 * @since 1.16
1477 *
1478 * @see #write(FitsOutputStream)
1479 */
1480 public void write(FitsFile file) throws IOException, FitsException {
1481 write((ArrayDataOutput) file);
1482 file.setLength(file.getFilePointer());
1483 }
1484
1485 /**
1486 * Writes the contents to a designated FITS output stream. It is up to the caller to close the stream as appropriate
1487 * after writing to it.
1488 *
1489 * @param out an output stream that supports FITS encoding.
1490 *
1491 * @throws FitsException if there were any errors writing the contents themselves.
1492 * @throws IOException if the underlying file could not be flushed or closed.
1493 *
1494 * @since 1.16
1495 *
1496 * @see #write(FitsFile)
1497 * @see #write(File)
1498 * @see #write(String)
1499 */
1500 public void write(FitsOutputStream out) throws IOException, FitsException {
1501 write((ArrayDataOutput) out);
1502 out.flush();
1503 }
1504
1505 /**
1506 * Writes the contents to a new file.
1507 *
1508 * @param file a file to which the FITS is to be written.
1509 *
1510 * @throws FitsException if there were any errors writing the contents themselves.
1511 * @throws IOException if the underlying output stream could not be created or closed.
1512 *
1513 * @see #write(FitsOutputStream)
1514 */
1515 public void write(File file) throws IOException, FitsException {
1516 try (FileOutputStream o = new FileOutputStream(file); FitsOutputStream fo = new FitsOutputStream(o)) {
1517 write(fo);
1518 o.flush();
1519 }
1520 }
1521
1522 /**
1523 * Re-writes all HDUs that have been loaded (and possibly modified) to the disk, if possible -- or else does
1524 * nothing. For HDUs that are in deferred mode (data unloaded and unchanged), only the header is re-written to disk.
1525 * Otherwise, both header and data is re-written. Of course, rewriting is possible only if the sizes of all headers
1526 * and data segments remain the same as before.
1527 *
1528 * @throws FitsException If one or more of the HDUs cannot be re-written, or if there was some other error
1529 * serializing the HDUs to disk.
1530 * @throws IOException If there was an I/O error accessing the output file.
1531 *
1532 * @since 1.17
1533 *
1534 * @see BasicHDU#rewriteable()
1535 */
1536 public void rewrite() throws FitsException, IOException {
1537 for (int i = 0; i < getNumberOfHDUs(); i++) {
1538 if (!getHDU(i).rewriteable()) {
1539 throw new FitsException("HDU[" + i + "] cannot be re-written in place. Aborting rewrite.");
1540 }
1541 }
1542
1543 for (int i = 0; i < getNumberOfHDUs(); i++) {
1544 getHDU(i).rewrite();
1545 }
1546 }
1547
1548 /**
1549 * Writes the contents to the specified file. It simply wraps {@link #write(File)} for convenience.
1550 *
1551 * @param fileName the file name/path
1552 *
1553 * @throws FitsException if there were any errors writing the contents themselves.
1554 * @throws IOException if the underlying stream could not be created or closed.
1555 *
1556 * @since 1.16
1557 *
1558 * @see #write(File)
1559 */
1560 public void write(String fileName) throws IOException, FitsException {
1561 write(new File(fileName));
1562 }
1563
1564 // TODO For DataOutputStream this one conflicts with write(DataOutput).
1565 // However
1566 // once that one is deprecated, this one can be exposed safely.
1567 // public void write(OutputStream os) throws IOException, FitsException {
1568 // write(new FitsOutputStream(os));
1569 // }
1570
1571 /**
1572 * Writes the contents to the specified output. This should not be exposed outside of this class, since the output
1573 * object must have FITS-specific encoding, and we can only make sure of that if this is called locally only.
1574 *
1575 * @param out the output with a FITS-specific encoding.
1576 *
1577 * @throws FitsException if the operation failed
1578 */
1579 private void write(ArrayDataOutput out) throws FitsException {
1580 for (BasicHDU<?> basicHDU : hduList) {
1581 basicHDU.write(out);
1582 }
1583 }
1584
1585 /**
1586 * @deprecated This method is poorly conceived as we cannot really write FITS to just any
1587 * <code>DataOutput</code> but only to specific {@link ArrayDataOutput}, which utilize
1588 * {@link nom.tam.util.FitsEncoder} to convert Java types to FITS binary format, such
1589 * as {@link FitsOutputStream} or {@link FitsFile} (or else a wrapped
1590 * <code>DataOutputStream</code>). As such, this method is inherently unsafe as it can
1591 * be used to create unreadable FITS files. It will be removed from a future major
1592 * release. Use one of the more appropriate other <code>write()</code> methods
1593 * instead. Writes the contents to an external file or stream. The file or stream
1594 * remains open and it is up to the caller to close it as appropriate.
1595 *
1596 * @param os A <code>DataOutput</code> stream.
1597 *
1598 * @throws FitsException if the operation failed
1599 *
1600 * @see #write(FitsFile)
1601 * @see #write(FitsOutputStream)
1602 * @see #write(File)
1603 * @see #write(String)
1604 */
1605 @Deprecated
1606 public void write(DataOutput os) throws FitsException {
1607 if (os instanceof FitsFile) {
1608 try {
1609 write((FitsFile) os);
1610 return;
1611 } catch (IOException e) {
1612 throw new FitsException("Error writing to FITS file: " + e, e);
1613 }
1614 }
1615
1616 if (os instanceof FitsOutputStream) {
1617 try {
1618 write((FitsOutputStream) os);
1619 return;
1620 } catch (IOException e) {
1621 throw new FitsException("Error writing to FITS output stream: " + e, e);
1622 }
1623 }
1624
1625 if (!(os instanceof DataOutputStream)) {
1626 throw new FitsException("Cannot create FitsOutputStream from class " + os.getClass().getName());
1627 }
1628
1629 try (FitsOutputStream fos = new FitsOutputStream((DataOutputStream) os)) {
1630 write(fos);
1631 } catch (IOException e) {
1632 throw new FitsException("Error writing to the FITS output stream: " + e, e);
1633 }
1634 }
1635
1636 @Override
1637 public void close() throws IOException {
1638 if (dataStr != null) {
1639 dataStr.close();
1640 }
1641 }
1642
1643 /**
1644 * set the checksum of a HDU.
1645 *
1646 * @param hdu the HDU to add a checksum
1647 *
1648 * @throws FitsException the checksum could not be added to the header
1649 *
1650 * @deprecated use {@link FitsCheckSum#setChecksum(BasicHDU)}
1651 */
1652 @Deprecated
1653 public static void setChecksum(BasicHDU<?> hdu) throws FitsException {
1654 FitsCheckSum.setChecksum(hdu);
1655 }
1656
1657 /**
1658 * calculate the checksum for the block of data
1659 *
1660 * @param data the data to create the checksum for
1661 *
1662 * @return the checksum
1663 *
1664 * @deprecated use {@link FitsCheckSum#checksum(byte[])}
1665 */
1666 @Deprecated
1667 public static long checksum(final byte[] data) {
1668 return FitsCheckSum.checksum(data);
1669 }
1670
1671 /**
1672 * Checks if the file ends before the current read positon, and if so, it may log a warning. This may happen with
1673 * {@link FitsFile} where the contract of {@link RandomAccess} allows for skipping ahead beyond the end of file,
1674 * since expanding the file is allowed when writing. Only a subsequent read call would fail.
1675 *
1676 * @param in the input from which the FITS content was read.
1677 *
1678 * @return <code>true</code> if the current read position is beyond the end-of-file, otherwise
1679 * <code>false</code>.
1680 *
1681 * @throws IOException if there was an IO error accessing the file or stream.
1682 *
1683 * @see ArrayDataInput#skip(long)
1684 * @see ArrayDataInput#skipBytes(int)
1685 * @see ArrayDataInput#skipAllBytes(long)
1686 *
1687 * @since 1.16
1688 */
1689 static boolean checkTruncated(ArrayDataInput in) throws IOException {
1690 if (!(in instanceof RandomAccess)) {
1691 // We cannot skip more than is available in an input stream.
1692 return false;
1693 }
1694
1695 RandomAccess f = (RandomAccess) in;
1696 long pos = f.getFilePointer();
1697 long len = f.length();
1698 if (pos > len) {
1699 LOG.log(Level.WARNING, "Premature file end at " + len + " (expected " + pos + ")", new Throwable());
1700 return true;
1701 }
1702 return false;
1703 }
1704 }