View Javadoc
1   /*
2    * #%L
3    * nom.tam FITS library
4    * %%
5    * Copyright (C) 1996 - 2024 nom-tam-fits
6    * %%
7    * This is free and unencumbered software released into the public domain.
8    *
9    * Anyone is free to copy, modify, publish, use, compile, sell, or
10   * distribute this software, either in source code form or as a compiled
11   * binary, for any purpose, commercial or non-commercial, and by any
12   * means.
13   *
14   * In jurisdictions that recognize copyright laws, the author or authors
15   * of this software dedicate any and all copyright interest in the
16   * software to the public domain. We make this dedication for the benefit
17   * of the public at large and to the detriment of our heirs and
18   * successors. We intend this dedication to be an overt act of
19   * relinquishment in perpetuity of all present and future rights to this
20   * software under copyright law.
21   *
22   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
26   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
27   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28   * OTHER DEALINGS IN THE SOFTWARE.
29   * #L%
30   */
31  
32  package nom.tam.util;
33  
34  import java.io.IOException;
35  import java.lang.reflect.Array;
36  import java.util.logging.Level;
37  import java.util.logging.Logger;
38  
39  import nom.tam.fits.FitsFactory;
40  import nom.tam.util.type.ElementType;
41  
42  /**
43   * Encodes select Java arrays into FITS binary format (<i>primarily for internal use</i>)
44   *
45   * @since 1.16
46   *
47   * @see   FitsDecoder
48   * @see   FitsFile
49   * @see   FitsInputStream
50   */
51  public class FitsEncoder extends OutputEncoder {
52  
53      private static final Logger LOG = Logger.getLogger(FitsEncoder.class.getName());
54  
55      /**
56       * The FITS byte value for the binary representation of a boolean 'true' value
57       */
58      private static final byte BYTE_TRUE = (byte) 'T';
59  
60      /**
61       * The FITS byte value for the binary representation of a boolean 'false' value
62       */
63      private static final byte BYTE_FALSE = (byte) 'F';
64  
65      /**
66       * Instantiates a new encoder from Java arrays to FITS binary output. To be used by subclass constructors only.
67       */
68      protected FitsEncoder() {
69          super();
70      }
71  
72      /**
73       * Instantiates a new FITS binary data encoder for converting Java arrays into FITS data representations
74       *
75       * @param o the FITS output.
76       */
77      public FitsEncoder(OutputWriter o) {
78          super(o);
79      }
80  
81      /**
82       * Returns the FITS byte value representing a logical value. This call supports <code>null</code> values, which are
83       * allowed by the FITS standard. FITS defines 'T' as true, 'F' as false, and 0 as null. Prior versions of this
84       * library have used the value 1 for true, and 0 for false. Therefore, this implementation will recognise both 'T'
85       * and 1 as <code>true</code>, but 0 will map to <code>null</code> and everything else will return
86       * <code>false</code>.
87       * 
88       * @param  b A java boolean value or <code>null</code>
89       * 
90       * @return   the FITS byte representation of a boolean value.
91       */
92      public static byte byteForBoolean(Boolean b) {
93          if (b == null) {
94              return (byte) 0;
95          }
96          return b ? BYTE_TRUE : BYTE_FALSE;
97      }
98  
99      /**
100      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
101      *                             arrays by this library only.
102      *
103      * @param      b           a boolean value or <code>null</code>.
104      *
105      * @throws     IOException if there was an IO error writing to the output.
106      */
107     @Deprecated
108     protected synchronized void writeBoolean(Boolean b) throws IOException {
109         write(byteForBoolean(b));
110     }
111 
112     /**
113      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
114      *                             arrays by this library only.
115      *
116      * @param      c           An ASCII character.
117      *
118      * @throws     IOException if there was an IO error writing to the output.
119      */
120     @Deprecated
121     protected synchronized void writeChar(int c) throws IOException {
122         if (FitsFactory.isUseUnicodeChars()) {
123             writeShort((short) c);
124         } else {
125             write(c & FitsIO.BYTE_MASK);
126         }
127     }
128 
129     /**
130      * Puts a boolean array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
131      * underlying output. The caller may put multiple data object into the conversion buffer before eventually calling
132      * {@link OutputBuffer#flush()} to ensure that everything is written to the output. Note, the this call may flush
133      * the contents of the conversion buffer to the output if it needs more conversion space than what is avaiable.
134      *
135      * @param  b           the Java array containing the values
136      * @param  start       the offset in the array from where to start converting values.
137      * @param  length      the number of values to convert to FITS representation
138      *
139      * @throws IOException if there was an IO error while trying to flush the conversion buffer to the stream before all
140      *                         elements were converted.
141      *
142      * @see                #byteForBoolean(Boolean)
143      * @see                #put(Boolean[], int, int)
144      * @see                #write(boolean[], int, int)
145      */
146     private void put(boolean[] b, int start, int length) throws IOException {
147         if (length == 1) {
148             write(byteForBoolean(b[start]));
149             return;
150         }
151 
152         byte[] ascii = new byte[length];
153         for (int i = 0; i < length; i++) {
154             ascii[i] = byteForBoolean(b[start + i]);
155         }
156         write(ascii, 0, length);
157     }
158 
159     /**
160      * Puts a boolean array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
161      * underlying output. The caller may put multiple data object into the conversion buffer before eventually calling
162      * {@link OutputBuffer#flush()} to ensure that everything is written to the output. Note, the this call may flush
163      * the contents of the conversion buffer to the output if it needs more conversion space than what is avaiable.
164      *
165      * @param  b           the Java array containing the values
166      * @param  start       the offset in the array from where to start converting values.
167      * @param  length      the number of values to convert to FITS representation
168      *
169      * @throws IOException if there was an IO error while trying to flush the conversion buffer to the stream before all
170      *                         elements were converted.
171      *
172      * @see                #byteForBoolean(Boolean)
173      * @see                #put(boolean[], int, int)
174      * @see                #write(Boolean[], int, int)
175      */
176     private void put(Boolean[] b, int start, int length) throws IOException {
177         if (length == 1) {
178             write(byteForBoolean(b[start]));
179             return;
180         }
181 
182         byte[] ascii = new byte[length];
183         for (int i = 0; i < length; i++) {
184             ascii[i] = byteForBoolean(b[start + i]);
185         }
186         write(ascii, 0, length);
187     }
188 
189     /**
190      * Puts a character array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
191      * underlying output. The caller may put multiple data object into the conversion buffer before eventually calling
192      * {@link OutputBuffer#flush()} to ensure that everything is written to the output. Note, the this call may flush
193      * the contents of the conversion buffer to the output if it needs more conversion space than what is avaiable.
194      *
195      * @param  b           the Java array containing the values
196      * @param  start       the offset in the array from where to start converting values.
197      * @param  length      the number of values to convert to FITS representation
198      *
199      * @throws IOException if there was an IO error while trying to flush the conversion buffer to the stream before all
200      *                         elements were converted.
201      *
202      * @see                #write(char[], int, int)
203      * @see                #put(String)
204      */
205     private void put(char[] c, int start, int length) throws IOException {
206         if (length == 1) {
207             if (ElementType.CHAR.size() == 1) {
208                 write((byte) c[start]);
209             } else {
210                 getOutputBuffer().putShort((short) c[start]);
211             }
212             return;
213         }
214 
215         if (ElementType.CHAR.size() == 1) {
216             byte[] ascii = new byte[length];
217             for (int i = 0; i < length; i++) {
218                 ascii[i] = (byte) c[start + i];
219             }
220             write(ascii, 0, length);
221         } else {
222             short[] s = new short[length];
223             for (int i = 0; i < length; i++) {
224                 s[i] = (short) c[start + i];
225             }
226             getOutputBuffer().put(s, 0, length);
227         }
228     }
229 
230     /**
231      * Puts a string array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
232      * underlying output. The caller may put multiple data object into the conversion buffer before eventually calling
233      * {@link OutputBuffer#flush()} to ensure that everything is written to the output. Note, the this call may flush
234      * the contents of the conversion buffer to the output if it needs more conversion space than what is avaiable.
235      *
236      * @param  b           the Java array containing the values
237      * @param  start       the offset in the array from where to start converting values.
238      * @param  length      the number of values to convert to FITS representation
239      *
240      * @throws IOException if there was an IO error while trying to flush the conversion buffer to the stream before all
241      *                         elements were converted.
242      *
243      * @see                #put(String)
244      */
245     private void put(String[] str, int start, int length) throws IOException {
246         length += start;
247         while (start < length) {
248             put(str[start++]);
249         }
250     }
251 
252     /**
253      * Puts a string into the conversion buffer. According to FITS standard, string should be represented by the
254      * restricted set of ASCII characters, or 1-byte per character. The caller may put multiple data object into the
255      * conversion buffer before eventually calling {@link OutputBuffer#flush()} to ensure that everything is written to
256      * the output. Note, the this call may flush the contents of the conversion buffer to the output if it needs more
257      * conversion space than what is avaiable.
258      *
259      * @param  str         the Java string
260      *
261      * @throws IOException if there was an IO error while trying to flush the conversion buffer to the stream before all
262      *                         elements were converted.
263      *
264      * @see                #writeBytes(String)
265      */
266     void put(String str) throws IOException {
267         OutputBuffer out = getOutputBuffer();
268         for (int i = 0; i < str.length(); i++) {
269             out.putByte((byte) str.charAt(i));
270         }
271     }
272 
273     /**
274      * See {@link ArrayDataOutput#write(boolean[], int, int)} for the general contract of this method. In FITS,
275      * <code>true</code> values are represented by the ASCII byte for 'T', whereas <code>false</code> is represented by
276      * the ASCII byte for 'F'.
277      *
278      * @param  b           array of booleans.
279      * @param  start       the index of the first element in the array to write
280      * @param  length      number of array elements to write
281      *
282      * @throws IOException if there was an IO error writing to the output
283      */
284     protected synchronized void write(boolean[] b, int start, int length) throws IOException {
285         put(b, start, length);
286         flush();
287     }
288 
289     /**
290      * See {@link ArrayDataOutput#write(Boolean[], int, int)} for the general contract of this method. In FITS,
291      * <code>true</code> values are represented by the ASCII byte for 'T', <code>false</code> is represented by the
292      * ASCII byte for 'F', while <code>null</code> values are represented by the value 0.
293      *
294      * @param  b           array of booleans.
295      * @param  start       the index of the first element in the array to write
296      * @param  length      number of array elements to write
297      *
298      * @throws IOException if there was an IO error writing to the output
299      */
300     protected synchronized void write(Boolean[] b, int start, int length) throws IOException {
301         put(b, start, length);
302         flush();
303     }
304 
305     /**
306      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
307      *                             arrays by this library only.
308      *
309      * @param      b           a single byte.
310      *
311      * @throws     IOException if there was an IO error writing to the output.
312      */
313     @Deprecated
314     protected synchronized void writeByte(int b) throws IOException {
315         write(b);
316     }
317 
318     /**
319      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
320      *                             arrays by this library only.
321      *
322      * @param      s           a 16-bit integer value.
323      *
324      * @throws     IOException if there was an IO error writing to the output.
325      */
326     @Deprecated
327     protected synchronized void writeShort(int s) throws IOException {
328         getOutputBuffer().putShort((short) s);
329         flush();
330     }
331 
332     /**
333      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
334      *                             arrays by this library only.
335      *
336      * @param      i           a 32-bit integer value.
337      *
338      * @throws     IOException if there was an IO error writing to the output.
339      */
340     @Deprecated
341     protected synchronized void writeInt(int i) throws IOException {
342         getOutputBuffer().putInt(i);
343         flush();
344     }
345 
346     /**
347      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
348      *                             arrays by this library only.
349      *
350      * @param      l           a 64-bit integer value.
351      *
352      * @throws     IOException if there was an IO error writing to the output.
353      */
354     @Deprecated
355     protected synchronized void writeLong(long l) throws IOException {
356         getOutputBuffer().putLong(l);
357         flush();
358     }
359 
360     /**
361      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally by this
362      *                             library only.
363      *
364      * @param      f           a single-precision (32-bit) floating point value.
365      *
366      * @throws     IOException if there was an IO error writing to the output.
367      */
368     @Deprecated
369     protected synchronized void writeFloat(float f) throws IOException {
370         getOutputBuffer().putFloat(f);
371         flush();
372     }
373 
374     /**
375      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
376      *                             arrays by this library only.
377      *
378      * @param      d           a double-precision (64-bit) floating point value.
379      *
380      * @throws     IOException if there was an IO error writing to the output.
381      */
382     @Deprecated
383     protected synchronized void writeDouble(double d) throws IOException {
384         getOutputBuffer().putDouble(d);
385         flush();
386     }
387 
388     /**
389      * Writes a Java string as a sequence of ASCII bytes to the output. FITS does not support unicode characters in its
390      * version of strings (character arrays), but instead it is restricted to the ASCII set of 1-byte characters.
391      *
392      * @param  s           the Java string
393      *
394      * @throws IOException if the string could not be fully written to the output
395      *
396      * @see                #writeChars(String)
397      */
398     protected synchronized void writeBytes(String s) throws IOException {
399         put(s);
400         flush();
401     }
402 
403     /**
404      * In FITS characters are usually represented as 1-byte ASCII, not as the 2-byte Java types. However, previous
405      * implementations if this library have erroneously written 2-byte characters into the FITS. For compatibility both
406      * the FITS standard of 1-byte ASCII and the old 2-byte behaviour are supported, and can be selected via
407      * {@link FitsFactory#setUseUnicodeChars(boolean)}.
408      *
409      * @param  s           a string containing ASCII-only characters
410      *
411      * @throws IOException if there was an IO error writing all the characters to the output.
412      *
413      * @see                #writeBytes(String)
414      * @see                FitsFactory#setUseUnicodeChars(boolean)
415      */
416     protected synchronized void writeChars(String s) throws IOException {
417         if (ElementType.CHAR.size() == 1) {
418             writeBytes(s);
419         } else {
420             OutputBuffer out = getOutputBuffer();
421             for (int i = 0; i < s.length(); i++) {
422                 out.putShort((short) s.charAt(i));
423             }
424             flush();
425         }
426 
427     }
428 
429     /**
430      * See {@link ArrayDataOutput#write(char[], int, int)} for the general contract of this method. In FITS characters
431      * are usually represented as 1-byte ASCII, not as the 2-byte Java types. However, previous implementations if this
432      * library have erroneously written 2-byte characters into the FITS. For compatibility both the FITS standard of
433      * 1-byte ASCII and the old 2-byte behaviour are supported, and can be selected via
434      * {@link FitsFactory#setUseUnicodeChars(boolean)}.
435      *
436      * @param  c           array of character (ASCII only is supported).
437      * @param  start       the index of the first element in the array to write
438      * @param  length      number of array elements to write
439      *
440      * @throws IOException if there was an IO error writing to the output
441      *
442      * @see                FitsFactory#setUseUnicodeChars(boolean)
443      */
444     protected synchronized void write(char[] c, int start, int length) throws IOException {
445         put(c, start, length);
446         flush();
447     }
448 
449     /**
450      * See {@link ArrayDataOutput#write(short[], int, int)} for a contract of this method.
451      *
452      * @param  s           array of 16-bit integers.
453      * @param  start       the index of the first element in the array to write
454      * @param  length      number of array elements to write
455      *
456      * @throws IOException if there was an IO error writing to the output
457      */
458     protected synchronized void write(short[] s, int start, int length) throws IOException {
459         getOutputBuffer().put(s, start, length);
460         flush();
461     }
462 
463     /**
464      * See {@link ArrayDataOutput#write(int[], int, int)} for a contract of this method.
465      *
466      * @param  i           array of 32-bit integers.
467      * @param  start       the index of the first element in the array to write
468      * @param  length      number of array elements to write
469      *
470      * @throws IOException if there was an IO error writing to the output
471      */
472     protected synchronized void write(int[] i, int start, int length) throws IOException {
473         getOutputBuffer().put(i, start, length);
474         flush();
475     }
476 
477     /**
478      * See {@link ArrayDataOutput#write(long[], int, int)} for a contract of this method.
479      *
480      * @param  l           array of 64-bit integers.
481      * @param  start       the index of the first element in the array to write
482      * @param  length      number of array elements to write
483      *
484      * @throws IOException if there was an IO error writing to the output
485      */
486     protected synchronized void write(long[] l, int start, int length) throws IOException {
487         getOutputBuffer().put(l, start, length);
488         flush();
489     }
490 
491     /**
492      * See {@link ArrayDataOutput#write(float[], int, int)} for a contract of this method.
493      *
494      * @param  f           array of single precision (32-bit) floating point values.
495      * @param  start       the index of the first element in the array to write
496      * @param  length      number of array elements to write
497      *
498      * @throws IOException if there was an IO error writing to the output
499      */
500     protected synchronized void write(float[] f, int start, int length) throws IOException {
501         getOutputBuffer().put(f, start, length);
502         flush();
503     }
504 
505     /**
506      * See {@link ArrayDataOutput#write(double[], int, int)} for a contract of this method.
507      *
508      * @param  d           array of double-precision (64-bit) floating point values.
509      * @param  start       the index of the first element in the array to write
510      * @param  length      number of array elements to write
511      *
512      * @throws IOException if there was an IO error writing to the output
513      */
514     protected synchronized void write(double[] d, int start, int length) throws IOException {
515         getOutputBuffer().put(d, start, length);
516         flush();
517     }
518 
519     /**
520      * See {@link ArrayDataOutput#write(String[], int, int)} for a contract of this method.
521      *
522      * @param  str         array of strings (containing ASCII characters only).
523      * @param  start       the index of the first element in the array to write
524      * @param  length      number of array elements to write
525      *
526      * @throws IOException if there was an IO error writing to the output
527      */
528     protected synchronized void write(String[] str, int start, int length) throws IOException {
529         length += start;
530         while (start < length) {
531             writeBytes(str[start++]);
532         }
533     }
534 
535     @Override
536     public synchronized void writeArray(Object o) throws IOException, IllegalArgumentException {
537         putArray(o);
538         flush();
539     }
540 
541     /**
542      * <p>
543      * Puts a Java array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
544      * underlying output. The argument may be any Java array of the types supported in FITS, including multi-dimensional
545      * arrays and heterogeneous arrays of arrays.
546      * </p>
547      * <p>
548      * The caller may put multiple data object into the conversion buffer before eventually calling
549      * {@link nom.tam.util.OutputEncoder.OutputBuffer#flush()} to ensure that everything is written to the output. Note,
550      * the this call may flush the contents of the conversion buffer to the output if it needs more conversion space
551      * than what is avaiable.
552      * </p>
553      *
554      * @param  o                        A Java array, including multi-dimensional arrays and heterogeneous arrays of
555      *                                      arrays.
556      *
557      * @throws IOException              if there was an IO error while trying to flush the conversion buffer to the
558      *                                      stream before all elements were converted.
559      * @throws IllegalArgumentException if the argument is not an array, or if it is or contains an element that does
560      *                                      not have a known FITS representation.
561      *
562      * @see                             #writeArray(Object)
563      */
564     protected void putArray(Object o) throws IOException, IllegalArgumentException {
565         if (o == null) {
566             return;
567         }
568 
569         if (o instanceof ComplexValue) {
570             putArray(((ComplexValue) o).toArray());
571             return;
572         }
573 
574         if (!o.getClass().isArray()) {
575             throw new IllegalArgumentException("Not an array: " + o.getClass().getName());
576         }
577 
578         int length = Array.getLength(o);
579         if (length == 0) {
580             return;
581         }
582 
583         if (o instanceof byte[]) {
584             getOutputBuffer().put((byte[]) o, 0, length);
585         } else if (o instanceof boolean[]) {
586             put((boolean[]) o, 0, length);
587         } else if (o instanceof char[]) {
588             put((char[]) o, 0, length);
589         } else if (o instanceof short[]) {
590             getOutputBuffer().put((short[]) o, 0, length);
591         } else if (o instanceof int[]) {
592             getOutputBuffer().put((int[]) o, 0, length);
593         } else if (o instanceof float[]) {
594             getOutputBuffer().put((float[]) o, 0, length);
595         } else if (o instanceof long[]) {
596             getOutputBuffer().put((long[]) o, 0, length);
597         } else if (o instanceof double[]) {
598             getOutputBuffer().put((double[]) o, 0, length);
599         } else if (o instanceof Boolean[]) {
600             put((Boolean[]) o, 0, length);
601         } else if (o instanceof String[]) {
602             put((String[]) o, 0, length);
603         } else {
604             Object[] array = (Object[]) o;
605             // Is this a multidimensional array? If so process recursively
606             for (int i = 0; i < length; i++) {
607                 putArray(array[i]);
608             }
609         }
610     }
611 
612     /**
613      * Returns the size of this object as the number of bytes in a FITS binary representation.
614      *
615      * @param  o the object
616      *
617      * @return   the number of bytes in the FITS binary representation of the object or 0 if the object has no FITS
618      *               representation. (Also elements not known to FITS will count as 0 sized).
619      */
620     public static long computeSize(Object o) {
621         if (o == null) {
622             return 0;
623         }
624 
625         if (o instanceof Object[]) {
626             long size = 0;
627             for (Object e : (Object[]) o) {
628                 size += computeSize(e);
629             }
630             return size;
631         }
632 
633         if (o instanceof ComplexValue) {
634             return 2 * (o instanceof ComplexValue.Float ? ElementType.FLOAT.size() : ElementType.DOUBLE.size());
635         }
636 
637         Class<?> type = o.getClass();
638         ElementType<?> eType = type.isArray() ? ElementType.forClass(type.getComponentType()) : ElementType.forClass(type);
639 
640         if (eType == ElementType.UNKNOWN) {
641             LOG.log(Level.WARNING, "computeSize() called with unknown type.",
642                     new IllegalArgumentException("Don't know FITS size of type " + type.getSimpleName()));
643         }
644 
645         if (eType.isVariableSize()) {
646             return eType.size(o);
647         }
648 
649         if (type.isArray()) {
650             return Array.getLength(o) * eType.size();
651         }
652 
653         return eType.size();
654     }
655 }