96749c1321
This reverts commit 39de459a86.
519 lines
20 KiB
Java
519 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2012 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.gallery3d.exif;
|
|
|
|
import android.util.Log;
|
|
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.FilterOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* This class provides a way to replace the Exif header of a JPEG image.
|
|
* <p>
|
|
* Below is an example of writing EXIF data into a file
|
|
*
|
|
* <pre>
|
|
* public static void writeExif(byte[] jpeg, ExifData exif, String path) {
|
|
* OutputStream os = null;
|
|
* try {
|
|
* os = new FileOutputStream(path);
|
|
* ExifOutputStream eos = new ExifOutputStream(os);
|
|
* // Set the exif header
|
|
* eos.setExifData(exif);
|
|
* // Write the original jpeg out, the header will be add into the file.
|
|
* eos.write(jpeg);
|
|
* } catch (FileNotFoundException e) {
|
|
* e.printStackTrace();
|
|
* } catch (IOException e) {
|
|
* e.printStackTrace();
|
|
* } finally {
|
|
* if (os != null) {
|
|
* try {
|
|
* os.close();
|
|
* } catch (IOException e) {
|
|
* e.printStackTrace();
|
|
* }
|
|
* }
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*/
|
|
class ExifOutputStream extends FilterOutputStream {
|
|
private static final String TAG = "ExifOutputStream";
|
|
private static final boolean DEBUG = false;
|
|
private static final int STREAMBUFFER_SIZE = 0x00010000; // 64Kb
|
|
|
|
private static final int STATE_SOI = 0;
|
|
private static final int STATE_FRAME_HEADER = 1;
|
|
private static final int STATE_JPEG_DATA = 2;
|
|
|
|
private static final int EXIF_HEADER = 0x45786966;
|
|
private static final short TIFF_HEADER = 0x002A;
|
|
private static final short TIFF_BIG_ENDIAN = 0x4d4d;
|
|
private static final short TIFF_LITTLE_ENDIAN = 0x4949;
|
|
private static final short TAG_SIZE = 12;
|
|
private static final short TIFF_HEADER_SIZE = 8;
|
|
private static final int MAX_EXIF_SIZE = 65535;
|
|
|
|
private ExifData mExifData;
|
|
private int mState = STATE_SOI;
|
|
private int mByteToSkip;
|
|
private int mByteToCopy;
|
|
private byte[] mSingleByteArray = new byte[1];
|
|
private ByteBuffer mBuffer = ByteBuffer.allocate(4);
|
|
private final ExifInterface mInterface;
|
|
|
|
protected ExifOutputStream(OutputStream ou, ExifInterface iRef) {
|
|
super(new BufferedOutputStream(ou, STREAMBUFFER_SIZE));
|
|
mInterface = iRef;
|
|
}
|
|
|
|
/**
|
|
* Sets the ExifData to be written into the JPEG file. Should be called
|
|
* before writing image data.
|
|
*/
|
|
protected void setExifData(ExifData exifData) {
|
|
mExifData = exifData;
|
|
}
|
|
|
|
/**
|
|
* Gets the Exif header to be written into the JPEF file.
|
|
*/
|
|
protected ExifData getExifData() {
|
|
return mExifData;
|
|
}
|
|
|
|
private int requestByteToBuffer(int requestByteCount, byte[] buffer
|
|
, int offset, int length) {
|
|
int byteNeeded = requestByteCount - mBuffer.position();
|
|
int byteToRead = length > byteNeeded ? byteNeeded : length;
|
|
mBuffer.put(buffer, offset, byteToRead);
|
|
return byteToRead;
|
|
}
|
|
|
|
/**
|
|
* Writes the image out. The input data should be a valid JPEG format. After
|
|
* writing, it's Exif header will be replaced by the given header.
|
|
*/
|
|
@Override
|
|
public void write(byte[] buffer, int offset, int length) throws IOException {
|
|
while ((mByteToSkip > 0 || mByteToCopy > 0 || mState != STATE_JPEG_DATA)
|
|
&& length > 0) {
|
|
if (mByteToSkip > 0) {
|
|
int byteToProcess = length > mByteToSkip ? mByteToSkip : length;
|
|
length -= byteToProcess;
|
|
mByteToSkip -= byteToProcess;
|
|
offset += byteToProcess;
|
|
}
|
|
if (mByteToCopy > 0) {
|
|
int byteToProcess = length > mByteToCopy ? mByteToCopy : length;
|
|
out.write(buffer, offset, byteToProcess);
|
|
length -= byteToProcess;
|
|
mByteToCopy -= byteToProcess;
|
|
offset += byteToProcess;
|
|
}
|
|
if (length == 0) {
|
|
return;
|
|
}
|
|
switch (mState) {
|
|
case STATE_SOI:
|
|
int byteRead = requestByteToBuffer(2, buffer, offset, length);
|
|
offset += byteRead;
|
|
length -= byteRead;
|
|
if (mBuffer.position() < 2) {
|
|
return;
|
|
}
|
|
mBuffer.rewind();
|
|
if (mBuffer.getShort() != JpegHeader.SOI) {
|
|
throw new IOException("Not a valid jpeg image, cannot write exif");
|
|
}
|
|
out.write(mBuffer.array(), 0, 2);
|
|
mState = STATE_FRAME_HEADER;
|
|
mBuffer.rewind();
|
|
writeExifData();
|
|
break;
|
|
case STATE_FRAME_HEADER:
|
|
// We ignore the APP1 segment and copy all other segments
|
|
// until SOF tag.
|
|
byteRead = requestByteToBuffer(4, buffer, offset, length);
|
|
offset += byteRead;
|
|
length -= byteRead;
|
|
// Check if this image data doesn't contain SOF.
|
|
if (mBuffer.position() == 2) {
|
|
short tag = mBuffer.getShort();
|
|
if (tag == JpegHeader.EOI) {
|
|
out.write(mBuffer.array(), 0, 2);
|
|
mBuffer.rewind();
|
|
}
|
|
}
|
|
if (mBuffer.position() < 4) {
|
|
return;
|
|
}
|
|
mBuffer.rewind();
|
|
short marker = mBuffer.getShort();
|
|
if (marker == JpegHeader.APP1) {
|
|
mByteToSkip = (mBuffer.getShort() & 0x0000ffff) - 2;
|
|
mState = STATE_JPEG_DATA;
|
|
} else if (!JpegHeader.isSofMarker(marker)) {
|
|
out.write(mBuffer.array(), 0, 4);
|
|
mByteToCopy = (mBuffer.getShort() & 0x0000ffff) - 2;
|
|
} else {
|
|
out.write(mBuffer.array(), 0, 4);
|
|
mState = STATE_JPEG_DATA;
|
|
}
|
|
mBuffer.rewind();
|
|
}
|
|
}
|
|
if (length > 0) {
|
|
out.write(buffer, offset, length);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes the one bytes out. The input data should be a valid JPEG format.
|
|
* After writing, it's Exif header will be replaced by the given header.
|
|
*/
|
|
@Override
|
|
public void write(int oneByte) throws IOException {
|
|
mSingleByteArray[0] = (byte) (0xff & oneByte);
|
|
write(mSingleByteArray);
|
|
}
|
|
|
|
/**
|
|
* Equivalent to calling write(buffer, 0, buffer.length).
|
|
*/
|
|
@Override
|
|
public void write(byte[] buffer) throws IOException {
|
|
write(buffer, 0, buffer.length);
|
|
}
|
|
|
|
private void writeExifData() throws IOException {
|
|
if (mExifData == null) {
|
|
return;
|
|
}
|
|
if (DEBUG) {
|
|
Log.v(TAG, "Writing exif data...");
|
|
}
|
|
ArrayList<ExifTag> nullTags = stripNullValueTags(mExifData);
|
|
createRequiredIfdAndTag();
|
|
int exifSize = calculateAllOffset();
|
|
if (exifSize + 8 > MAX_EXIF_SIZE) {
|
|
throw new IOException("Exif header is too large (>64Kb)");
|
|
}
|
|
OrderedDataOutputStream dataOutputStream = new OrderedDataOutputStream(out);
|
|
dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);
|
|
dataOutputStream.writeShort(JpegHeader.APP1);
|
|
dataOutputStream.writeShort((short) (exifSize + 8));
|
|
dataOutputStream.writeInt(EXIF_HEADER);
|
|
dataOutputStream.writeShort((short) 0x0000);
|
|
if (mExifData.getByteOrder() == ByteOrder.BIG_ENDIAN) {
|
|
dataOutputStream.writeShort(TIFF_BIG_ENDIAN);
|
|
} else {
|
|
dataOutputStream.writeShort(TIFF_LITTLE_ENDIAN);
|
|
}
|
|
dataOutputStream.setByteOrder(mExifData.getByteOrder());
|
|
dataOutputStream.writeShort(TIFF_HEADER);
|
|
dataOutputStream.writeInt(8);
|
|
writeAllTags(dataOutputStream);
|
|
writeThumbnail(dataOutputStream);
|
|
for (ExifTag t : nullTags) {
|
|
mExifData.addTag(t);
|
|
}
|
|
}
|
|
|
|
private ArrayList<ExifTag> stripNullValueTags(ExifData data) {
|
|
ArrayList<ExifTag> nullTags = new ArrayList<ExifTag>();
|
|
for(ExifTag t : data.getAllTags()) {
|
|
if (t.getValue() == null && !ExifInterface.isOffsetTag(t.getTagId())) {
|
|
data.removeTag(t.getTagId(), t.getIfd());
|
|
nullTags.add(t);
|
|
}
|
|
}
|
|
return nullTags;
|
|
}
|
|
|
|
private void writeThumbnail(OrderedDataOutputStream dataOutputStream) throws IOException {
|
|
if (mExifData.hasCompressedThumbnail()) {
|
|
dataOutputStream.write(mExifData.getCompressedThumbnail());
|
|
} else if (mExifData.hasUncompressedStrip()) {
|
|
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
|
dataOutputStream.write(mExifData.getStrip(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void writeAllTags(OrderedDataOutputStream dataOutputStream) throws IOException {
|
|
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_0), dataOutputStream);
|
|
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_EXIF), dataOutputStream);
|
|
IfdData interoperabilityIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
|
if (interoperabilityIfd != null) {
|
|
writeIfd(interoperabilityIfd, dataOutputStream);
|
|
}
|
|
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
|
if (gpsIfd != null) {
|
|
writeIfd(gpsIfd, dataOutputStream);
|
|
}
|
|
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
|
if (ifd1 != null) {
|
|
writeIfd(mExifData.getIfdData(IfdId.TYPE_IFD_1), dataOutputStream);
|
|
}
|
|
}
|
|
|
|
private void writeIfd(IfdData ifd, OrderedDataOutputStream dataOutputStream)
|
|
throws IOException {
|
|
ExifTag[] tags = ifd.getAllTags();
|
|
dataOutputStream.writeShort((short) tags.length);
|
|
for (ExifTag tag : tags) {
|
|
dataOutputStream.writeShort(tag.getTagId());
|
|
dataOutputStream.writeShort(tag.getDataType());
|
|
dataOutputStream.writeInt(tag.getComponentCount());
|
|
if (DEBUG) {
|
|
Log.v(TAG, "\n" + tag.toString());
|
|
}
|
|
if (tag.getDataSize() > 4) {
|
|
dataOutputStream.writeInt(tag.getOffset());
|
|
} else {
|
|
ExifOutputStream.writeTagValue(tag, dataOutputStream);
|
|
for (int i = 0, n = 4 - tag.getDataSize(); i < n; i++) {
|
|
dataOutputStream.write(0);
|
|
}
|
|
}
|
|
}
|
|
dataOutputStream.writeInt(ifd.getOffsetToNextIfd());
|
|
for (ExifTag tag : tags) {
|
|
if (tag.getDataSize() > 4) {
|
|
ExifOutputStream.writeTagValue(tag, dataOutputStream);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int calculateOffsetOfIfd(IfdData ifd, int offset) {
|
|
offset += 2 + ifd.getTagCount() * TAG_SIZE + 4;
|
|
ExifTag[] tags = ifd.getAllTags();
|
|
for (ExifTag tag : tags) {
|
|
if (tag.getDataSize() > 4) {
|
|
tag.setOffset(offset);
|
|
offset += tag.getDataSize();
|
|
}
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
private void createRequiredIfdAndTag() throws IOException {
|
|
// IFD0 is required for all file
|
|
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
|
|
if (ifd0 == null) {
|
|
ifd0 = new IfdData(IfdId.TYPE_IFD_0);
|
|
mExifData.addIfdData(ifd0);
|
|
}
|
|
ExifTag exifOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_EXIF_IFD);
|
|
if (exifOffsetTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_EXIF_IFD);
|
|
}
|
|
ifd0.setTag(exifOffsetTag);
|
|
|
|
// Exif IFD is required for all files.
|
|
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
|
|
if (exifIfd == null) {
|
|
exifIfd = new IfdData(IfdId.TYPE_IFD_EXIF);
|
|
mExifData.addIfdData(exifIfd);
|
|
}
|
|
|
|
// GPS IFD
|
|
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
|
if (gpsIfd != null) {
|
|
ExifTag gpsOffsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_GPS_IFD);
|
|
if (gpsOffsetTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_GPS_IFD);
|
|
}
|
|
ifd0.setTag(gpsOffsetTag);
|
|
}
|
|
|
|
// Interoperability IFD
|
|
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
|
if (interIfd != null) {
|
|
ExifTag interOffsetTag = mInterface
|
|
.buildUninitializedTag(ExifInterface.TAG_INTEROPERABILITY_IFD);
|
|
if (interOffsetTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_INTEROPERABILITY_IFD);
|
|
}
|
|
exifIfd.setTag(interOffsetTag);
|
|
}
|
|
|
|
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
|
|
|
// thumbnail
|
|
if (mExifData.hasCompressedThumbnail()) {
|
|
|
|
if (ifd1 == null) {
|
|
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
|
|
mExifData.addIfdData(ifd1);
|
|
}
|
|
|
|
ExifTag offsetTag = mInterface
|
|
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
|
|
if (offsetTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
|
|
}
|
|
|
|
ifd1.setTag(offsetTag);
|
|
ExifTag lengthTag = mInterface
|
|
.buildUninitializedTag(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
|
if (lengthTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
|
|
}
|
|
|
|
lengthTag.setValue(mExifData.getCompressedThumbnail().length);
|
|
ifd1.setTag(lengthTag);
|
|
|
|
// Get rid of tags for uncompressed if they exist.
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
|
|
} else if (mExifData.hasUncompressedStrip()) {
|
|
if (ifd1 == null) {
|
|
ifd1 = new IfdData(IfdId.TYPE_IFD_1);
|
|
mExifData.addIfdData(ifd1);
|
|
}
|
|
int stripCount = mExifData.getStripCount();
|
|
ExifTag offsetTag = mInterface.buildUninitializedTag(ExifInterface.TAG_STRIP_OFFSETS);
|
|
if (offsetTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_STRIP_OFFSETS);
|
|
}
|
|
ExifTag lengthTag = mInterface
|
|
.buildUninitializedTag(ExifInterface.TAG_STRIP_BYTE_COUNTS);
|
|
if (lengthTag == null) {
|
|
throw new IOException("No definition for crucial exif tag: "
|
|
+ ExifInterface.TAG_STRIP_BYTE_COUNTS);
|
|
}
|
|
long[] lengths = new long[stripCount];
|
|
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
|
lengths[i] = mExifData.getStrip(i).length;
|
|
}
|
|
lengthTag.setValue(lengths);
|
|
ifd1.setTag(offsetTag);
|
|
ifd1.setTag(lengthTag);
|
|
// Get rid of tags for compressed if they exist.
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
|
|
ifd1.removeTag(ExifInterface
|
|
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
|
|
} else if (ifd1 != null) {
|
|
// Get rid of offset and length tags if there is no thumbnail.
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS));
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS));
|
|
ifd1.removeTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT));
|
|
ifd1.removeTag(ExifInterface
|
|
.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH));
|
|
}
|
|
}
|
|
|
|
private int calculateAllOffset() {
|
|
int offset = TIFF_HEADER_SIZE;
|
|
IfdData ifd0 = mExifData.getIfdData(IfdId.TYPE_IFD_0);
|
|
offset = calculateOffsetOfIfd(ifd0, offset);
|
|
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD)).setValue(offset);
|
|
|
|
IfdData exifIfd = mExifData.getIfdData(IfdId.TYPE_IFD_EXIF);
|
|
offset = calculateOffsetOfIfd(exifIfd, offset);
|
|
|
|
IfdData interIfd = mExifData.getIfdData(IfdId.TYPE_IFD_INTEROPERABILITY);
|
|
if (interIfd != null) {
|
|
exifIfd.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD))
|
|
.setValue(offset);
|
|
offset = calculateOffsetOfIfd(interIfd, offset);
|
|
}
|
|
|
|
IfdData gpsIfd = mExifData.getIfdData(IfdId.TYPE_IFD_GPS);
|
|
if (gpsIfd != null) {
|
|
ifd0.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD)).setValue(offset);
|
|
offset = calculateOffsetOfIfd(gpsIfd, offset);
|
|
}
|
|
|
|
IfdData ifd1 = mExifData.getIfdData(IfdId.TYPE_IFD_1);
|
|
if (ifd1 != null) {
|
|
ifd0.setOffsetToNextIfd(offset);
|
|
offset = calculateOffsetOfIfd(ifd1, offset);
|
|
}
|
|
|
|
// thumbnail
|
|
if (mExifData.hasCompressedThumbnail()) {
|
|
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT))
|
|
.setValue(offset);
|
|
offset += mExifData.getCompressedThumbnail().length;
|
|
} else if (mExifData.hasUncompressedStrip()) {
|
|
int stripCount = mExifData.getStripCount();
|
|
long[] offsets = new long[stripCount];
|
|
for (int i = 0; i < mExifData.getStripCount(); i++) {
|
|
offsets[i] = offset;
|
|
offset += mExifData.getStrip(i).length;
|
|
}
|
|
ifd1.getTag(ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS)).setValue(
|
|
offsets);
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
static void writeTagValue(ExifTag tag, OrderedDataOutputStream dataOutputStream)
|
|
throws IOException {
|
|
switch (tag.getDataType()) {
|
|
case ExifTag.TYPE_ASCII:
|
|
byte buf[] = tag.getStringByte();
|
|
if (buf.length == tag.getComponentCount()) {
|
|
buf[buf.length - 1] = 0;
|
|
dataOutputStream.write(buf);
|
|
} else {
|
|
dataOutputStream.write(buf);
|
|
dataOutputStream.write(0);
|
|
}
|
|
break;
|
|
case ExifTag.TYPE_LONG:
|
|
case ExifTag.TYPE_UNSIGNED_LONG:
|
|
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
|
dataOutputStream.writeInt((int) tag.getValueAt(i));
|
|
}
|
|
break;
|
|
case ExifTag.TYPE_RATIONAL:
|
|
case ExifTag.TYPE_UNSIGNED_RATIONAL:
|
|
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
|
dataOutputStream.writeRational(tag.getRational(i));
|
|
}
|
|
break;
|
|
case ExifTag.TYPE_UNDEFINED:
|
|
case ExifTag.TYPE_UNSIGNED_BYTE:
|
|
buf = new byte[tag.getComponentCount()];
|
|
tag.getBytes(buf);
|
|
dataOutputStream.write(buf);
|
|
break;
|
|
case ExifTag.TYPE_UNSIGNED_SHORT:
|
|
for (int i = 0, n = tag.getComponentCount(); i < n; i++) {
|
|
dataOutputStream.writeShort((short) tag.getValueAt(i));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|