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