View Javadoc
1   package nom.tam.image;
2   
3   import java.io.EOFException;
4   
5   /*
6    * #%L
7    * nom.tam FITS library
8    * %%
9    * Copyright (C) 2004 - 2024 nom-tam-fits
10   * %%
11   * This is free and unencumbered software released into the public domain.
12   *
13   * Anyone is free to copy, modify, publish, use, compile, sell, or
14   * distribute this software, either in source code form or as a compiled
15   * binary, for any purpose, commercial or non-commercial, and by any
16   * means.
17   *
18   * In jurisdictions that recognize copyright laws, the author or authors
19   * of this software dedicate any and all copyright interest in the
20   * software to the public domain. We make this dedication for the benefit
21   * of the public at large and to the detriment of our heirs and
22   * successors. We intend this dedication to be an overt act of
23   * relinquishment in perpetuity of all present and future rights to this
24   * software under copyright law.
25   *
26   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32   * OTHER DEALINGS IN THE SOFTWARE.
33   * #L%
34   */
35  
36  import java.io.IOException;
37  import java.lang.reflect.Array;
38  import java.util.Arrays;
39  
40  import nom.tam.util.ArrayDataOutput;
41  import nom.tam.util.ArrayFuncs;
42  import nom.tam.util.RandomAccess;
43  import nom.tam.util.type.ElementType;
44  
45  /**
46   * <p>
47   * Standard image tiling implementation. FITS tiles are always 2-dimentional, but really images of any dimensions may be
48   * covered with such tiles.
49   * </p>
50   * <p>
51   * Modified May 2, 2000 by T. McGlynn to permit tiles that go off the edge of the image.
52   * </p>
53   */
54  public abstract class StandardImageTiler implements ImageTiler {
55      /**
56       * @param  dims The dimensions of the array.
57       * @param  pos  The index requested.
58       *
59       * @return      the offset of a given position.
60       */
61      public static long getOffset(int[] dims, int[] pos) {
62  
63          long offset = 0;
64          for (int i = 0; i < dims.length; i++) {
65              if (i > 0) {
66                  offset *= dims[i];
67              }
68              offset += pos[i];
69          }
70          return offset;
71      }
72  
73      /**
74       * Increment the offset within the position array. Note that we never look at the last index since we copy data a
75       * block at a time and not byte by byte.
76       *
77       * @param  start   The starting corner values.
78       * @param  current The current offsets.
79       * @param  lengths The desired dimensions of the subset.
80       *
81       * @return         <code>true</code> if the current array was changed
82       */
83      protected static boolean incrementPosition(int[] start, int[] current, int[] lengths) {
84          final int[] steps = new int[start.length];
85          Arrays.fill(steps, 1);
86          return StandardImageTiler.incrementPosition(start, current, lengths, steps);
87      }
88  
89      /**
90       * Increment the offset within the position array. Note that we never look at the last index since we copy data a
91       * block at a time and not byte by byte.
92       *
93       * @param  start   The starting corner values.
94       * @param  current The current offsets.
95       * @param  lengths The desired dimensions of the subset.
96       * @param  steps   The desired number of steps to take until the next position.
97       *
98       * @return         <code>true</code> if the current array was changed
99       */
100     protected static boolean incrementPosition(int[] start, int[] current, int[] lengths, int[] steps) {
101         for (int i = start.length - 2; i >= 0; i--) {
102             if (current[i] - start[i] < lengths[i] - steps[i]) {
103                 current[i] += steps[i];
104                 if (start.length - 1 - (i + 1) >= 0) {
105                     System.arraycopy(start, i + 1, current, i + 1, start.length - 1 - (i + 1));
106                 }
107                 return true;
108             }
109         }
110         return false;
111     }
112 
113     private final RandomAccess randomAccessFile;
114 
115     private final long fileOffset;
116 
117     private final int[] dims;
118 
119     private final Class<?> base;
120 
121     /**
122      * Create a tiler.
123      *
124      * @param f          The random access device from which image data may be read. This may be null if the tile
125      *                       information is available from memory.
126      * @param fileOffset The file offset within the RandomAccess device at which the data begins.
127      * @param dims       The actual dimensions of the image.
128      * @param base       The base class (should be a primitive type) of the image.
129      */
130     public StandardImageTiler(RandomAccess f, long fileOffset, int[] dims, Class<?> base) {
131         randomAccessFile = f;
132         this.fileOffset = fileOffset;
133         this.dims = dims;
134         this.base = base;
135     }
136 
137     /**
138      * File a tile segment from a file using a default value for striding.
139      *
140      * @param  output       The output to send data. This can be an ArrayDataOutput to stream data to and prevent memory
141      *                          consumption of a tile being in memory.
142      * @param  delta        The offset from the beginning of the image in bytes.
143      * @param  outputOffset The index into the output array.
144      * @param  segment      The number of elements to be read for this segment.
145      *
146      * @throws IOException  if the underlying stream failed
147      */
148     protected void fillFileData(Object output, long delta, int outputOffset, int segment) throws IOException {
149         fillFileData(output, delta, outputOffset, segment, 1);
150     }
151 
152     /**
153      * File a tile segment from a file, jumping each step number of values to the next read.
154      *
155      * @param  output       The output to send data. This can be an ArrayDataOutput to stream data to and prevent memory
156      *                          consumption of a tile being in memory.
157      * @param  delta        The offset from the beginning of the image in bytes.
158      * @param  outputOffset The index into the output array.
159      * @param  segment      The number of elements to be read for this segment.
160      * @param  step         The number of jumps until the next read. Only works for streaming out data.
161      *
162      * @throws EOFException if already at the end of file / stream
163      * @throws IOException  if the underlying stream failed
164      */
165     protected void fillFileData(Object output, long delta, int outputOffset, int segment, int step) throws IOException {
166         if (output instanceof ArrayDataOutput) {
167             this.fillFileData((ArrayDataOutput) output, delta, segment, step);
168         } else {
169             randomAccessFile.seek(fileOffset + delta);
170             int got = 0;
171 
172             if (base == float.class) {
173                 got = randomAccessFile.read((float[]) output, outputOffset, segment);
174             } else if (base == int.class) {
175                 got = randomAccessFile.read((int[]) output, outputOffset, segment);
176             } else if (base == short.class) {
177                 got = randomAccessFile.read((short[]) output, outputOffset, segment);
178             } else if (base == double.class) {
179                 got = randomAccessFile.read((double[]) output, outputOffset, segment);
180             } else if (base == byte.class) {
181                 got = randomAccessFile.read((byte[]) output, outputOffset, segment);
182             } else if (base == long.class) {
183                 got = randomAccessFile.read((long[]) output, outputOffset, segment);
184             } else {
185                 throw new IOException("Invalid type for tile array");
186             }
187 
188             if (got < 0) {
189                 throw new EOFException();
190             }
191         }
192     }
193 
194     /**
195      * File a tile segment from a file into the given stream. This will deal only with bytes to avoid having to check
196      * the base type and calling a specific method. Converting the base type to a byte is a simple multiplication
197      * operation anyway. Uses a default value for striding (1).
198      *
199      * @param  output      The output stream.
200      * @param  delta       The offset from the beginning of the image in bytes.
201      * @param  segment     The number of elements to be read for this segment.
202      *
203      * @throws IOException if the underlying stream failed
204      */
205     protected void fillFileData(ArrayDataOutput output, long delta, int segment) throws IOException {
206         fillFileData(output, delta, segment, 1);
207     }
208 
209     /**
210      * File a tile segment from a file into the given stream. This will deal only with bytes to avoid having to check
211      * the base type and calling a specific method. Converting the base type to a byte is a simple multiplication
212      * operation anyway.
213      *
214      * @param  output      The output stream.
215      * @param  delta       The offset from the beginning of the image in bytes.
216      * @param  segment     The number of elements to be read for this segment.
217      * @param  step        The number of elements until the next read.
218      *
219      * @throws IOException if the underlying stream failed
220      *
221      * @since              1.18
222      */
223     protected void fillFileData(ArrayDataOutput output, long delta, int segment, int step) throws IOException {
224         final int byteSize = ElementType.forClass(base).size();
225 
226         // Subtract one from the step since when we read from a stream, an actual
227         // "step" only exists if it's greater
228         // than 1.
229         final int stepSize = (step - 1) * byteSize;
230         randomAccessFile.seek(fileOffset + delta);
231 
232         // One value at a time
233         final byte[] buffer = new byte[byteSize];
234         long seekOffset = randomAccessFile.position();
235         int bytesRead = 0;
236 
237         // This is the byte count that will be read.
238         final int expectedBytes = segment * byteSize;
239         while (bytesRead < expectedBytes) {
240             // Prepare for the next read by seeking to the next step
241             randomAccessFile.seek(seekOffset);
242             final int currReadByteCount = randomAccessFile.read(buffer, 0, buffer.length);
243 
244             // Stop if there is no more to read.
245             if (currReadByteCount < 0) {
246                 break;
247             }
248             output.write(buffer, 0, currReadByteCount);
249             seekOffset = randomAccessFile.position() + stepSize;
250             bytesRead += currReadByteCount + stepSize;
251         }
252 
253         output.flush();
254     }
255 
256     /**
257      * Fill a single segment from memory. This routine is called recursively to handle multidimensional arrays. E.g., if
258      * data is three-dimensional, this will recurse two levels until we get a call with a single dimensional datum. At
259      * that point the appropriate data will be copied into the output. Uses a default value for striding (1).
260      *
261      * @param  data         The in-memory image data.
262      * @param  posits       The current position for which data is requested.
263      * @param  length       The size of the segments.
264      * @param  output       The output tile.
265      * @param  outputOffset The current offset into the output tile.
266      * @param  dim          The current dimension being
267      *
268      * @throws IOException  If the output is a stream and there is an I/O error.
269      */
270     protected void fillMemData(Object data, int[] posits, int length, Object output, int outputOffset, int dim)
271             throws IOException {
272         fillMemData(data, posits, length, output, outputOffset, dim, 1);
273     }
274 
275     /**
276      * Fill a single segment from memory. This routine is called recursively to handle multidimensional arrays. E.g., if
277      * data is three-dimensional, this will recurse two levels until we get a call with a single dimensional datum. At
278      * that point the appropriate data will be copied into the output, jumping the number of step values.
279      *
280      * @param  data         The in-memory image data.
281      * @param  posits       The current position for which data is requested.
282      * @param  length       The size of the segments.
283      * @param  output       The output tile.
284      * @param  outputOffset The current offset into the output tile.
285      * @param  dim          The current dimension being
286      * @param  step         The number of jumps to the next value.
287      *
288      * @throws IOException  If the output is a stream and there is an I/O error.
289      *
290      * @since               1.18
291      */
292     protected void fillMemData(Object data, int[] posits, int length, Object output, int outputOffset, int dim, int step)
293             throws IOException {
294 
295         if (data instanceof Object[]) {
296 
297             Object[] xo = (Object[]) data;
298             fillMemData(xo[posits[dim]], posits, length, output, outputOffset, dim + 1, step);
299 
300         } else {
301 
302             // Adjust the spacing for the actual copy.
303             int startFrom = posits[dim];
304             int startTo = outputOffset;
305             int copyLength = length;
306 
307             if (posits[dim] < 0) {
308                 startFrom -= posits[dim];
309                 startTo -= posits[dim];
310                 copyLength += posits[dim];
311             }
312             if (posits[dim] + length > dims[dim]) {
313                 copyLength -= posits[dim] + length - dims[dim];
314             }
315 
316             if (output instanceof ArrayDataOutput) {
317                 // Intentionally missing char and boolean here as they are not
318                 // valid BITPIX values.
319                 final ArrayDataOutput arrayDataOutput = ((ArrayDataOutput) output);
320                 for (int i = startFrom; i < startFrom + copyLength; i += step) {
321                     if (base == float.class) {
322                         arrayDataOutput.writeFloat(Array.getFloat(data, i));
323                     } else if (base == int.class) {
324                         arrayDataOutput.writeInt(Array.getInt(data, i));
325                     } else if (base == double.class) {
326                         arrayDataOutput.writeDouble(Array.getDouble(data, i));
327                     } else if (base == long.class) {
328                         arrayDataOutput.writeLong(Array.getLong(data, i));
329                     } else if (base == short.class) {
330                         arrayDataOutput.writeShort(Array.getShort(data, i));
331                     } else if (base == byte.class) {
332                         arrayDataOutput.writeByte(Array.getByte(data, i));
333                     }
334                 }
335 
336                 arrayDataOutput.flush();
337             } else {
338                 ArrayFuncs.copy(data, startFrom, output, startTo, copyLength, step);
339             }
340         }
341     }
342 
343     /**
344      * Fill the subset using a default value for striding.
345      *
346      * @param  data        The memory-resident data image. This may be null if the image is to be read from a file. This
347      *                         should be a multidimensional primitive array.
348      * @param  o           The tile to be filled. This is a simple primitive array, or an ArrayDataOutput instance.
349      * @param  newDims     The dimensions of the full image.
350      * @param  corners     The indices of the corner of the image.
351      * @param  lengths     The dimensions of the subset.
352      *
353      * @throws IOException if the underlying stream failed
354      */
355     protected void fillTile(Object data, Object o, int[] newDims, int[] corners, int[] lengths) throws IOException {
356         final int[] steps = new int[corners.length];
357         Arrays.fill(steps, 1);
358         fillTile(data, o, newDims, corners, lengths, steps);
359     }
360 
361     /**
362      * Fill the subset, jumping each step value to the next read.
363      *
364      * @param  data        The memory-resident data image. This may be null if the image is to be read from a file. This
365      *                         should be a multidimensional primitive array.
366      * @param  o           The tile to be filled. This is a simple primitive array, or an ArrayDataOutput instance.
367      * @param  newDims     The dimensions of the full image.
368      * @param  corners     The indices of the corner of the image.
369      * @param  lengths     The dimensions of the subset.
370      * @param  steps       The number of steps to take until the next read in each axis.
371      *
372      * @throws IOException if the underlying stream failed
373      */
374     protected void fillTile(Object data, Object o, int[] newDims, int[] corners, int[] lengths, int[] steps)
375             throws IOException {
376 
377         int n = newDims.length;
378         int[] posits = new int[n];
379         final boolean isStreaming = (o instanceof ArrayDataOutput);
380 
381         // TODO: When streaming out to an ArrayDataOutput, use this tiler's base
382         // class to determine the element size.
383         // TODO: If that is not sufficient, then maybe it needs to be passed in?
384         // TODO: jenkinsd 2022.12.21
385         //
386         final int baseLength = isStreaming ? ElementType.forClass(base).size() : ArrayFuncs.getBaseLength(o);
387 
388         int segment = lengths[n - 1];
389         int segmentStep = steps[n - 1];
390 
391         System.arraycopy(corners, 0, posits, 0, n);
392         long currentOffset = 0;
393         if (data == null) {
394             currentOffset = randomAccessFile.getFilePointer();
395         }
396 
397         int outputOffset = 0;
398 
399         // Flag to indicate something was written out. This is only relevant if
400         // the output is an ArrayDataOutput.
401         boolean hasNoOverlap = true;
402 
403         do {
404 
405             // This implies there is some overlap
406             // in the last index (in conjunction
407             // with other tests)
408 
409             int mx = newDims.length - 1;
410             boolean validSegment = posits[mx] + lengths[mx] >= 0 && posits[mx] < newDims[mx];
411 
412             // Don't do anything for the current
413             // segment if anything but the
414             // last index is out of range.
415 
416             if (validSegment) {
417                 for (int i = 0; i < mx; i++) {
418                     if (posits[i] < 0 || posits[i] >= newDims[i]) {
419                         validSegment = false;
420                         break;
421                     }
422                 }
423             }
424 
425             if (validSegment) {
426                 hasNoOverlap = false;
427                 if (data != null) {
428                     fillMemData(data, posits, segment, o, outputOffset, 0, segmentStep);
429                 } else {
430                     long offset = getOffset(newDims, posits) * baseLength;
431 
432                     // Point to offset at real beginning
433                     // of segment
434                     int actualLen = segment;
435                     long actualOffset = offset;
436                     int actualOutput = outputOffset;
437                     if (posits[mx] < 0) {
438                         actualOffset -= (long) posits[mx] * baseLength;
439                         actualOutput -= posits[mx];
440                         actualLen += posits[mx];
441                     }
442                     if (posits[mx] + segment > newDims[mx]) {
443                         actualLen -= posits[mx] + segment - newDims[mx];
444                     }
445                     fillFileData(o, actualOffset, actualOutput, actualLen, segmentStep);
446                 }
447             }
448             if (!isStreaming) {
449                 outputOffset += segment;
450             }
451 
452         } while (incrementPosition(corners, posits, lengths, steps));
453         if (data == null) {
454             randomAccessFile.seek(currentOffset);
455         }
456 
457         if (isStreaming && hasNoOverlap) {
458             throw new IOException("Sub-image not within image");
459         }
460     }
461 
462     @Override
463     public Object getCompleteImage() throws IOException {
464 
465         if (randomAccessFile == null) {
466             throw new IOException("Attempt to read from null file");
467         }
468         long currentOffset = randomAccessFile.getFilePointer();
469         Object o = ArrayFuncs.newInstance(base, dims);
470         randomAccessFile.seek(fileOffset);
471         randomAccessFile.readImage(o);
472         randomAccessFile.seek(currentOffset);
473         return o;
474     }
475 
476     /**
477      * See if we can get the image data from memory. This may be overridden by other classes, notably in
478      * nom.tam.fits.ImageData.
479      *
480      * @return the image data
481      */
482     protected abstract Object getMemoryImage();
483 
484     @Override
485     public Object getTile(int[] corners, int[] lengths) throws IOException {
486         final int[] steps = new int[corners.length];
487         Arrays.fill(steps, 1);
488         return getTile(corners, lengths, steps);
489     }
490 
491     @Override
492     public Object getTile(int[] corners, int[] lengths, int[] steps) throws IOException {
493 
494         if (corners.length != dims.length || lengths.length != dims.length) {
495             throw new IOException("Inconsistent sub-image request");
496         }
497 
498         int arraySize = 1;
499         for (int i = 0; i < dims.length; i++) {
500 
501             if (corners[i] < 0 || lengths[i] < 0 || corners[i] + lengths[i] > dims[i]) {
502                 throw new IOException("Sub-image not within image");
503             }
504             if (steps[i] < 1) {
505                 throw new IOException("Step value cannot be less than 1.");
506             }
507 
508             arraySize *= lengths[i];
509         }
510 
511         Object outArray = ArrayFuncs.newInstance(base, arraySize);
512 
513         getTile(outArray, corners, lengths, steps);
514         return outArray;
515     }
516 
517     @Override
518     public void getTile(Object output, int[] corners, int[] lengths) throws IOException {
519         final int[] steps = new int[corners.length];
520         Arrays.fill(steps, 1);
521         this.getTile(output, corners, lengths, steps);
522     }
523 
524     @Override
525     public void getTile(Object output, int[] corners, int[] lengths, int[] steps) throws IOException {
526         Object data = getMemoryImage();
527 
528         if (data == null && randomAccessFile == null) {
529             throw new IOException("No data source for tile subset");
530         }
531 
532         fillTile(data, output, dims, corners, lengths, steps);
533     }
534 }