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 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 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  c           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  str         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 void write(boolean[] b, int start, int length) throws IOException {
285         synchronized (lock) {
286             put(b, start, length);
287             flush();
288         }
289     }
290 
291     /**
292      * See {@link ArrayDataOutput#write(Boolean[], int, int)} for the general contract of this method. In FITS,
293      * <code>true</code> values are represented by the ASCII byte for 'T', <code>false</code> is represented by the
294      * ASCII byte for 'F', while <code>null</code> values are represented by the value 0.
295      *
296      * @param  b           array of booleans.
297      * @param  start       the index of the first element in the array to write
298      * @param  length      number of array elements to write
299      *
300      * @throws IOException if there was an IO error writing to the output
301      */
302     protected void write(Boolean[] b, int start, int length) throws IOException {
303         synchronized (lock) {
304             put(b, start, length);
305             flush();
306         }
307     }
308 
309     /**
310      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
311      *                             arrays by this library only.
312      *
313      * @param      b           a single byte.
314      *
315      * @throws     IOException if there was an IO error writing to the output.
316      */
317     @Deprecated
318     protected void writeByte(int b) throws IOException {
319         write(b);
320     }
321 
322     /**
323      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
324      *                             arrays by this library only.
325      *
326      * @param      s           a 16-bit integer value.
327      *
328      * @throws     IOException if there was an IO error writing to the output.
329      */
330     @Deprecated
331     protected void writeShort(int s) throws IOException {
332         synchronized (lock) {
333             getOutputBuffer().putShort((short) s);
334             flush();
335         }
336     }
337 
338     /**
339      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
340      *                             arrays by this library only.
341      *
342      * @param      i           a 32-bit integer value.
343      *
344      * @throws     IOException if there was an IO error writing to the output.
345      */
346     @Deprecated
347     protected void writeInt(int i) throws IOException {
348         synchronized (lock) {
349             getOutputBuffer().putInt(i);
350             flush();
351         }
352     }
353 
354     /**
355      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
356      *                             arrays by this library only.
357      *
358      * @param      l           a 64-bit integer value.
359      *
360      * @throws     IOException if there was an IO error writing to the output.
361      */
362     @Deprecated
363     protected void writeLong(long l) throws IOException {
364         synchronized (lock) {
365             getOutputBuffer().putLong(l);
366             flush();
367         }
368     }
369 
370     /**
371      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally by this
372      *                             library only.
373      *
374      * @param      f           a single-precision (32-bit) floating point value.
375      *
376      * @throws     IOException if there was an IO error writing to the output.
377      */
378     @Deprecated
379     protected void writeFloat(float f) throws IOException {
380         synchronized (lock) {
381             getOutputBuffer().putFloat(f);
382             flush();
383         }
384     }
385 
386     /**
387      * @deprecated             (<i>for internal use</i>) Low-level reading/writing should be handled internally as
388      *                             arrays by this library only.
389      *
390      * @param      d           a double-precision (64-bit) floating point value.
391      *
392      * @throws     IOException if there was an IO error writing to the output.
393      */
394     @Deprecated
395     protected void writeDouble(double d) throws IOException {
396         synchronized (lock) {
397             getOutputBuffer().putDouble(d);
398             flush();
399         }
400     }
401 
402     /**
403      * Writes a Java string as a sequence of ASCII bytes to the output. FITS does not support unicode characters in its
404      * version of strings (character arrays), but instead it is restricted to the ASCII set of 1-byte characters.
405      *
406      * @param  s           the Java string
407      *
408      * @throws IOException if the string could not be fully written to the output
409      *
410      * @see                #writeChars(String)
411      */
412     protected void writeBytes(String s) throws IOException {
413         synchronized (lock) {
414             put(s);
415             flush();
416         }
417     }
418 
419     /**
420      * In FITS characters are usually represented as 1-byte ASCII, not as the 2-byte Java types. However, previous
421      * implementations if this library have erroneously written 2-byte characters into the FITS. For compatibility both
422      * the FITS standard of 1-byte ASCII and the old 2-byte behaviour are supported, and can be selected via
423      * {@link FitsFactory#setUseUnicodeChars(boolean)}.
424      *
425      * @param  s           a string containing ASCII-only characters
426      *
427      * @throws IOException if there was an IO error writing all the characters to the output.
428      *
429      * @see                #writeBytes(String)
430      * @see                FitsFactory#setUseUnicodeChars(boolean)
431      */
432     protected void writeChars(String s) throws IOException {
433         if (ElementType.CHAR.size() == 1) {
434             writeBytes(s);
435         } else {
436             synchronized (lock) {
437                 OutputBuffer out = getOutputBuffer();
438                 for (int i = 0; i < s.length(); i++) {
439                     out.putShort((short) s.charAt(i));
440                 }
441                 flush();
442             }
443         }
444     }
445 
446     /**
447      * See {@link ArrayDataOutput#write(char[], int, int)} for the general contract of this method. In FITS characters
448      * are usually represented as 1-byte ASCII, not as the 2-byte Java types. However, previous implementations if this
449      * library have erroneously written 2-byte characters into the FITS. For compatibility both the FITS standard of
450      * 1-byte ASCII and the old 2-byte behaviour are supported, and can be selected via
451      * {@link FitsFactory#setUseUnicodeChars(boolean)}.
452      *
453      * @param  c           array of character (ASCII only is supported).
454      * @param  start       the index of the first element in the array to write
455      * @param  length      number of array elements to write
456      *
457      * @throws IOException if there was an IO error writing to the output
458      *
459      * @see                FitsFactory#setUseUnicodeChars(boolean)
460      */
461     protected void write(char[] c, int start, int length) throws IOException {
462         synchronized (lock) {
463             put(c, start, length);
464             flush();
465         }
466     }
467 
468     /**
469      * See {@link ArrayDataOutput#write(short[], int, int)} for a contract of this method.
470      *
471      * @param  s           array of 16-bit integers.
472      * @param  start       the index of the first element in the array to write
473      * @param  length      number of array elements to write
474      *
475      * @throws IOException if there was an IO error writing to the output
476      */
477     protected void write(short[] s, int start, int length) throws IOException {
478         synchronized (lock) {
479             getOutputBuffer().put(s, start, length);
480             flush();
481         }
482     }
483 
484     /**
485      * See {@link ArrayDataOutput#write(int[], int, int)} for a contract of this method.
486      *
487      * @param  i           array of 32-bit integers.
488      * @param  start       the index of the first element in the array to write
489      * @param  length      number of array elements to write
490      *
491      * @throws IOException if there was an IO error writing to the output
492      */
493     protected void write(int[] i, int start, int length) throws IOException {
494         synchronized (lock) {
495             getOutputBuffer().put(i, start, length);
496             flush();
497         }
498     }
499 
500     /**
501      * See {@link ArrayDataOutput#write(long[], int, int)} for a contract of this method.
502      *
503      * @param  l           array of 64-bit integers.
504      * @param  start       the index of the first element in the array to write
505      * @param  length      number of array elements to write
506      *
507      * @throws IOException if there was an IO error writing to the output
508      */
509     protected void write(long[] l, int start, int length) throws IOException {
510         synchronized (lock) {
511             getOutputBuffer().put(l, start, length);
512             flush();
513         }
514     }
515 
516     /**
517      * See {@link ArrayDataOutput#write(float[], int, int)} for a contract of this method.
518      *
519      * @param  f           array of single precision (32-bit) floating point values.
520      * @param  start       the index of the first element in the array to write
521      * @param  length      number of array elements to write
522      *
523      * @throws IOException if there was an IO error writing to the output
524      */
525     protected void write(float[] f, int start, int length) throws IOException {
526         synchronized (lock) {
527             getOutputBuffer().put(f, start, length);
528             flush();
529         }
530     }
531 
532     /**
533      * See {@link ArrayDataOutput#write(double[], int, int)} for a contract of this method.
534      *
535      * @param  d           array of double-precision (64-bit) floating point values.
536      * @param  start       the index of the first element in the array to write
537      * @param  length      number of array elements to write
538      *
539      * @throws IOException if there was an IO error writing to the output
540      */
541     protected void write(double[] d, int start, int length) throws IOException {
542         synchronized (lock) {
543             getOutputBuffer().put(d, start, length);
544             flush();
545         }
546     }
547 
548     /**
549      * See {@link ArrayDataOutput#write(String[], int, int)} for a contract of this method.
550      *
551      * @param  str         array of strings (containing ASCII characters only).
552      * @param  start       the index of the first element in the array to write
553      * @param  length      number of array elements to write
554      *
555      * @throws IOException if there was an IO error writing to the output
556      */
557     protected void write(String[] str, int start, int length) throws IOException {
558         length += start;
559         synchronized (lock) {
560             while (start < length) {
561                 writeBytes(str[start++]);
562             }
563         }
564     }
565 
566     @Override
567     public void writeArray(Object o) throws IOException, IllegalArgumentException {
568         synchronized (lock) {
569             putArray(o);
570             flush();
571         }
572     }
573 
574     /**
575      * <p>
576      * Puts a Java array into the conversion buffer, but with no guarantee of flushing the conversion buffer to the
577      * underlying output. The argument may be any Java array of the types supported in FITS, including multi-dimensional
578      * arrays and heterogeneous arrays of arrays.
579      * </p>
580      * <p>
581      * The caller may put multiple data object into the conversion buffer before eventually calling
582      * {@link nom.tam.util.OutputEncoder#flush()} to ensure that everything is written to the output. Note, the this
583      * call may flush the contents of the conversion buffer to the output if it needs more conversion space than what is
584      * avaiable.
585      * </p>
586      *
587      * @param  o                        A Java array, including multi-dimensional arrays and heterogeneous arrays of
588      *                                      arrays.
589      *
590      * @throws IOException              if there was an IO error while trying to flush the conversion buffer to the
591      *                                      stream before all elements were converted.
592      * @throws IllegalArgumentException if the argument is not an array, or if it is or contains an element that does
593      *                                      not have a known FITS representation.
594      *
595      * @see                             #writeArray(Object)
596      */
597     protected void putArray(Object o) throws IOException, IllegalArgumentException {
598         if (o == null) {
599             return;
600         }
601 
602         if (o instanceof ComplexValue) {
603             putArray(((ComplexValue) o).toArray());
604             return;
605         }
606 
607         if (!o.getClass().isArray()) {
608             throw new IllegalArgumentException("Not an array: " + o.getClass().getName());
609         }
610 
611         int length = Array.getLength(o);
612         if (length == 0) {
613             return;
614         }
615 
616         if (o instanceof byte[]) {
617             getOutputBuffer().put((byte[]) o, 0, length);
618         } else if (o instanceof boolean[]) {
619             put((boolean[]) o, 0, length);
620         } else if (o instanceof char[]) {
621             put((char[]) o, 0, length);
622         } else if (o instanceof short[]) {
623             getOutputBuffer().put((short[]) o, 0, length);
624         } else if (o instanceof int[]) {
625             getOutputBuffer().put((int[]) o, 0, length);
626         } else if (o instanceof float[]) {
627             getOutputBuffer().put((float[]) o, 0, length);
628         } else if (o instanceof long[]) {
629             getOutputBuffer().put((long[]) o, 0, length);
630         } else if (o instanceof double[]) {
631             getOutputBuffer().put((double[]) o, 0, length);
632         } else if (o instanceof Boolean[]) {
633             put((Boolean[]) o, 0, length);
634         } else if (o instanceof String[]) {
635             put((String[]) o, 0, length);
636         } else {
637             Object[] array = (Object[]) o;
638             // Is this a multidimensional array? If so process recursively
639             for (int i = 0; i < length; i++) {
640                 putArray(array[i]);
641             }
642         }
643     }
644 
645     /**
646      * Returns the size of this object as the number of bytes in a FITS binary representation.
647      *
648      * @param  o the object
649      *
650      * @return   the number of bytes in the FITS binary representation of the object or 0 if the object has no FITS
651      *               representation. (Also elements not known to FITS will count as 0 sized).
652      */
653     public static long computeSize(Object o) {
654         if (o == null) {
655             return 0;
656         }
657 
658         if (o instanceof Object[]) {
659             long size = 0;
660             for (Object e : (Object[]) o) {
661                 size += computeSize(e);
662             }
663             return size;
664         }
665 
666         if (o instanceof ComplexValue) {
667             return 2L * (o instanceof ComplexValue.Float ? ElementType.FLOAT.size() : ElementType.DOUBLE.size());
668         }
669 
670         Class<?> type = o.getClass();
671         ElementType<?> eType = type.isArray() ? ElementType.forClass(type.getComponentType()) : ElementType.forClass(type);
672 
673         if (eType == ElementType.UNKNOWN) {
674             LOG.log(Level.WARNING, "computeSize() called with unknown type.",
675                     new IllegalArgumentException("Don't know FITS size of type " + type.getSimpleName()));
676         }
677 
678         if (eType.isVariableSize()) {
679             return eType.size(o);
680         }
681 
682         if (type.isArray()) {
683             return (long) Array.getLength(o) * eType.size();
684         }
685 
686         return eType.size();
687     }
688 }