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 }