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