-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTIFFEncoder.java
More file actions
282 lines (256 loc) · 11.1 KB
/
TIFFEncoder.java
File metadata and controls
282 lines (256 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
/* TIFFEncoder.java */
/* DO NOT CHANGE THIS FILE. */
/* YOUR SUBMISSION MUST WORK CORRECTLY WITH _OUR_ COPY OF THIS FILE. */
/* You may wish to make temporary changes or insert println() statements */
/* while testing your code. When you're finished testing and debugging, */
/* though, make sure your code works with the original version of this file. */
/**
* The TIFFEncoder class allows us to write a TIFF file from a pixel array
* in PixImage format or from a run-length encoding in RunLengthEncoding
* format. A TIFF file written from a RunLengthEncoding is compressed as
* a run-length encoding, so it may be much shorter than a TIFF file written
* from a PixImage.
*
* @author Joel Galenson
**/
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import javax.imageio.stream.FileImageOutputStream;
public class TIFFEncoder {
/**
* The TiffType enum represents the type of a value of a TIFF field,
* which can have a variety of sizes.
*/
private static enum TiffType { SHORT, LONG }
/**
* getTypeInt() returns the integer flag for the specified TIFF type, which
* is used to specify which type the value has.
*
* @param type the type whose integer flag we want.
* @return the integer representing the given type.
*/
private static int getTypeInt(TiffType type) {
switch (type) {
case SHORT:
return 3;
case LONG:
return 4;
default:
throw new IllegalArgumentException();
}
}
/**
* writeLeftAlignedValue() writes a given value of the given type into
* a given stream as a left-justified four-byte record.
* See Section 2 (page 15) of the TIFF spec for details.
*
* @param stream the stream representing the file being written.
* @param type the type of the value.
* @param val the value to write.
* @throws IOException
*/
private static void writeLeftAlignedValue(FileImageOutputStream stream,
TiffType type, int val)
throws IOException {
switch (type) {
case SHORT:
stream.writeShort(val);
stream.writeShort(0);
break;
case LONG:
stream.writeInt(val);
break;
default: // There are other possible types, but we're not using them.
throw new IllegalArgumentException();
}
}
/**
* writeValueTag() writes an image file directory (IFD) entry whose value
* fits into the Value Offset.
* See Section 2 (page 15) of the TIFF spec for more details.
*
* @param stream the stream representing the file being written.
* @param tag the tag that identifies the field.
* @param type the type of the value.
* @param value the value of the field.
* @throws IOException
*/
private static void writeValueTag(FileImageOutputStream stream, int tag,
TiffType type, int value)
throws IOException {
stream.writeShort(tag);
stream.writeShort(getTypeInt(type));
stream.writeInt(1);
writeLeftAlignedValue(stream, type, value);
}
/**
* writeOffsetTag() writes an image file directory (IFD) entry whose value
* does not fit into the Value Offset, so we store it at another offset.
* See Section 2 (page 15) of the TIFF spec for more details.
*
* @param stream the stream representing the file being written.
* @param tag the tag that identifies the field.
* @param type the type of the value.
* @param count the number of values of the indicated type.
* @param offset the offset in the file where the actual value is stored.
* @throws IOException
*/
private static void writeOffsetTag(FileImageOutputStream stream, int tag,
TiffType type, int count, int offset)
throws IOException {
stream.writeShort(tag);
stream.writeShort(getTypeInt(type));
stream.writeInt(count);
// The offset is always a LONG (a 32-bit TiffType).
writeLeftAlignedValue(stream, TiffType.LONG, offset);
}
/**
* writeTIFF() writes the specified data into a TIFF file.
* For more details, see the TIFF spec at
* http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf.
* This code adapted from http://paulbourke.net/dataformats/tiff/.
*
* @param data the bytes of the image data.
* @param width the width of the image.
* @param height the height of the image.
* @param filename the name of the file to write.
* @param isCompressed true if the data is compressed in PackBits format;
* false if it is stored uncompressed.
*/
private static void writeTIFF(ArrayList<Short> data, int width, int height,
String filename, boolean isCompressed) {
// For simplicity, we hardcode the number of directories we're writing.
final int NUM_DIRS = 10;
// The size (in bytes) of various parts of TIFF images.
final int HEADER_SIZE = 8;
final int DIR_SIZE = 12;
try {
FileImageOutputStream stream =
new FileImageOutputStream(new File(filename));
// Write the header.
stream.writeShort(0x4d4d); // Big-endian byte order.
stream.writeShort(42); // Magic number for TIFF files.
stream.writeInt(data.size() + HEADER_SIZE); // Offset of image file dir.
// Write the image data.
for (Short datum: data) {
stream.writeByte(datum);
}
// Write the footer, including an image file directory (IFD).
stream.writeShort(NUM_DIRS); // Number of image file directory entries.
// TODO: width and height and others below could be LONG (32 bits).
// IFD entry 0: Image width.
writeValueTag(stream, 256, TiffType.SHORT, width);
// IFD entry 1: Image height.
writeValueTag(stream, 257, TiffType.SHORT, height);
// IFD entry 2: Bits per sample.
writeOffsetTag(stream, 258, TiffType.SHORT, 3,
data.size() + HEADER_SIZE + DIR_SIZE * NUM_DIRS + 6);
// IFD entry 3: Compression tag. 1 means no compression.
// 32773 means "PackBits compression", a run-length encoding.
writeValueTag(stream, 259, TiffType.SHORT, isCompressed ? 32773 : 1);
// IFD entry 4: Photometric tag. 2 means it's a full-color RGB image.
writeValueTag(stream, 262, TiffType.SHORT, 2);
// IFD entry 5: "StripOffsets". The byte offset of the image.
writeValueTag(stream, 273, TiffType.LONG, HEADER_SIZE);
// IFD entry 6: Samples per pixel. 3 for red, green, and blue.
writeValueTag(stream, 277, TiffType.SHORT, 3);
// IFD entry 7: Rows per strip. Our image is encoded as just one strip.
writeValueTag(stream, 278, TiffType.SHORT, height);
// IFD entry 8: "Strip byte count"; number of bytes in the image.
writeValueTag(stream, 279, TiffType.LONG, data.size());
// IFD entry 9: Planar configuration. 1 means each pixel is continuous
// (as opposed to separate sections for red, green, and blue).
writeValueTag(stream, 284, TiffType.SHORT, 1);
// Four bytes of zero signify that there are no more IFDs.
stream.writeInt(0);
// Write the "bits per sample" data for IFD entry 2 (above).
// There are 8 bits for red, 8 for green, and 8 for blue.
for (int i = 0; i < 3; i++) {
stream.writeShort(8);
}
stream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* writeTIFF() writes the specified PixImage into an uncompressed TIFF file.
*
* @param image the PixImage.
* @param filename the name of the file to write.
*/
public static void writeTIFF(PixImage image, String filename) {
ArrayList<Short> pixels =
new ArrayList<Short>(image.getWidth() * image.getHeight() * 3);
// Note that our for loops iterate in this order so we write by rows.
for (int j = 0; j < image.getHeight(); j++) {
for (int i = 0; i < image.getWidth(); i++) {
pixels.add(image.getRed(i, j));
pixels.add(image.getGreen(i, j));
pixels.add(image.getBlue(i, j));
}
}
writeTIFF(pixels, image.getWidth(), image.getHeight(), filename, false);
}
/**
* writeTIFF() writes the given image data into a compressed TIFF file.
*
* @param rle a run-length encoding of the image data.
* @param filename the name of the file to write.
*/
public static void writeTIFF(RunLengthEncoding rle, String filename) {
ArrayList<Short> pixels = new ArrayList<Short>();
int currentX = 0; // x-position of the next pixel.
for (RunIterator it = rle.iterator(); it.hasNext(); ) {
int[] run = it.next();
// The TIFF format can compress repeated bytes, so it can express a run
// of grayscale values in compressed form; but it cannot compress
// repeated red-green-blue triples if the red, green, and blue values are
// not all the same. So we check for a grayscale value (in which the
// red, green, and blue values are equal).
if (run[1] == run[2] && run[1] == run[3]) {
// It's a grayscale run. We can write the run in a compressed format.
int i = 0;
while (i < run[0] * 3) { // run[0] is the number of pixels in the run.
// Figure the number of bytes to write in one run. Note that it is
// always a factor of 3.
int curCount = Math.min(Math.min(run[0] * 3 - i, 126),
(rle.getWidth() - currentX) * 3);
pixels.add((short) (1 - curCount)); // # of times value is repeated.
pixels.add((short) run[1]); // The value that is repeated.
// The TIFF format does not allow you to compress across row
// boundaries, so we must keep track of the current column so we can
// break a run into smaller runs if it spans multiple rows.
currentX = (currentX + curCount / 3) % rle.getWidth();
i += curCount;
}
} else {
// Not grayscale. We must write every pixel individually.
// But we can still encode them as a combined literal run.
int i = 0;
while (i < run[0] * 3) { // run[0] is the number of pixels in the run.
// Figure the number of bytes to write in one literal. Note that it
// is always a factor of 3.
int curCount = Math.min(Math.min(run[0] * 3 - i, 126),
(rle.getWidth() - currentX) * 3);
pixels.add((short) (curCount - 1)); // Number of literal values.
for (int j = 0; j < curCount / 3; j++) { // The literal values.
pixels.add((short) run[1]); // Red.
pixels.add((short) run[2]); // Green.
pixels.add((short) run[3]); // Blue.
}
// The TIFF format does not allow you to compress across row
// boundaries, so we must keep track of the current column so we can
// break a run into smaller runs if it spans multiple rows.
currentX = (currentX + curCount / 3) % rle.getWidth();
i += curCount;
}
}
}
writeTIFF(pixels, rle.getWidth(), rle.getHeight(), filename, true);
}
}