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 }