5

In our app users have been uploading millions of images for years using (roughly) this code:

BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(postFilePath, bmOptions);
Bitmap roughBitmap = BitmapFactory.decodeFile(postFilePath, bmOptions);

ByteArrayOutputStream stream = new ByteArrayOutputStream();

roughBitmap.compress(Bitmap.CompressFormat.JPEG, 70, stream);
InputStream fis = new ByteArrayInputStream(stream.toByteArray());

int fileSize = stream.toByteArray().length;
conn.setRequestProperty("Content-Length", Integer.toString(fileSize));
conn.setFixedLengthStreamingMode(fileSize);

...

if (fis != null) {
    byte[] buf = new byte[10240];

    int read;

    while ((read = fis.read(buf)) > 0) {
        os.write(buf, 0, read);
        totalBytesRead += read;
        if (uploadProgressListener != null) {
            try {
                uploadProgressListener.onBytesUploaded(read);
            } catch (Exception e) {
                Log.e(e);
            }
        }
    }

    fis.close();
}

Recently we saw the need to preserve the Exif data of uploaded images. The problem is that the image Exif data is lost when compressing the bitmap. I thought of using ExifInterface for extracting this data from the original file:

ExifInterface oldExif = new ExifInterface(postFilePath);
String value = oldExif.getAttribute(ExifInterface.TAG_DATETIME);

..and then adding it to the InputStream fis and then continue uploading the file. The problem is that ExifInterface cannot save Exif data to an InputStream.

How can Exif data be retained in the images when they'er uploaded to the server?

It's not a duplicate: Just to clarify deeper, I've tried using the suggested duplicate question by using this method:

public static void copyExif(String originalPath, InputStream newStream) throws IOException {

    String[] attributes = new String[]
            {
                    ExifInterface.TAG_DATETIME,
                    ExifInterface.TAG_DATETIME_DIGITIZED,
                    ExifInterface.TAG_EXPOSURE_TIME,
                    ExifInterface.TAG_FLASH,
                    ExifInterface.TAG_FOCAL_LENGTH,
                    ExifInterface.TAG_GPS_ALTITUDE,
                    ExifInterface.TAG_GPS_ALTITUDE_REF,
                    ExifInterface.TAG_GPS_DATESTAMP,
                    ExifInterface.TAG_GPS_LATITUDE,
                    ExifInterface.TAG_GPS_LATITUDE_REF,
                    ExifInterface.TAG_GPS_LONGITUDE,
                    ExifInterface.TAG_GPS_LONGITUDE_REF,
                    ExifInterface.TAG_GPS_PROCESSING_METHOD,
                    ExifInterface.TAG_GPS_TIMESTAMP,
                    ExifInterface.TAG_MAKE,
                    ExifInterface.TAG_MODEL,
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.TAG_SUBSEC_TIME,
                    ExifInterface.TAG_WHITE_BALANCE
            };

    ExifInterface oldExif = new ExifInterface(originalPath);
    ExifInterface newExif = new ExifInterface(newStream);

    if (attributes.length > 0) {
        for (int i = 0; i < attributes.length; i++) {
            String value = oldExif.getAttribute(attributes[i]);
            if (value != null)
                newExif.setAttribute(attributes[i], value);
        }
        newExif.saveAttributes();
    }
}

.. but got the exception java.io.IOException: ExifInterface does not support saving attributes for the current input. after newExif.saveAttributes(); because I'm trying to save the attributes to an InputStream. How else can I do it?

Bhargav Rao
  • 50,140
  • 28
  • 121
  • 140
Ambran
  • 2,367
  • 4
  • 31
  • 46
  • 1
    Possible duplicate of [How to save Exif data after bitmap compression in Android](https://stackoverflow.com/questions/23762133/how-to-save-exif-data-after-bitmap-compression-in-android) – amuttsch Jul 05 '17 at 13:52
  • I have seen this one. Went after the suggestion, but as I wrote `ExifInterface` can only save to images, which is my problem here, so it's not a duplicate. – Ambran Jul 05 '17 at 13:57
  • 1
    I don't see your problem. Create an `ExifInterface` for both your original file and the compressed one (create a new `Bitmap` from the outputstream) and use `exifComp.setAttribute(TAG_..., exifOrig(TAG_...));` and save it with `exifComp.save()`. Afterwards, get the outputstream from the compressed file. – amuttsch Jul 05 '17 at 14:01
  • @amuttsch, do you mean to save the compressed file first, then save the exif attributes to it and then read a stream from it again? I can du that, but was hoping to avoid having to save the compressed file to the storage in the process. – Ambran Jul 05 '17 at 14:12
  • 1
    You can get an inputstream from an outputstream and use `BitmapFactory`, see: https://stackoverflow.com/questions/29286599/get-an-input-stream-from-an-output-stream – amuttsch Jul 05 '17 at 14:20

4 Answers4

9

My solution:

As @amuttsch and @CommonsWare suggested, I:

  1. saved the scaled/compressed bitmap to a temp file
  2. copied the exif from the original file to the temp file
  3. converted the temp file to a byte array and sent it to upload

.. then I found out that the server strips the Exif again while generating image variants :-P but that's another story which server guys are now working to correct.

Main code:

...
// Copy original Exif to scaledBitmap
String tempFilePath = getTempFilePath(postFilePath);
try {
    FileOutputStream out = new FileOutputStream(tempFilePath);
    scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 70, out);
    copyExif(postFilePath, tempFilePath);
} catch (Exception e) {
    e.printStackTrace();
}

// Get stream from temp (exif loaded) file
File tempFile = new File(tempFilePath);
byte[] byteFile = readFile(tempFile);
fis = new ByteArrayInputStream(byteFile);

// Remove the temp file
boolean deleted = tempFile.delete();

// Finalize
int fileSize = byteFile.length;
conn.setRequestProperty("Content-Length", Integer.toString(fileSize));
conn.setFixedLengthStreamingMode(fileSize);
...

getTempFilePath():

private String getTempFilePath(String filename) {
    String temp = "_temp";
    int dot = filename.lastIndexOf(".");
    String ext = filename.substring(dot + 1);

    if (dot == -1 || !ext.matches("\\w+")) {
        filename += temp;
    } else {
        filename = filename.substring(0, dot) + temp + "." + ext;
    }

    return filename;
}

copyExif():

public static void copyExif(String originalPath, String newPath) throws IOException {

    String[] attributes = new String[]
            {
                    ExifInterface.TAG_DATETIME,
                    ExifInterface.TAG_DATETIME_DIGITIZED,
                    ExifInterface.TAG_EXPOSURE_TIME,
                    ExifInterface.TAG_FLASH,
                    ExifInterface.TAG_FOCAL_LENGTH,
                    ExifInterface.TAG_GPS_ALTITUDE,
                    ExifInterface.TAG_GPS_ALTITUDE_REF,
                    ExifInterface.TAG_GPS_DATESTAMP,
                    ExifInterface.TAG_GPS_LATITUDE,
                    ExifInterface.TAG_GPS_LATITUDE_REF,
                    ExifInterface.TAG_GPS_LONGITUDE,
                    ExifInterface.TAG_GPS_LONGITUDE_REF,
                    ExifInterface.TAG_GPS_PROCESSING_METHOD,
                    ExifInterface.TAG_GPS_TIMESTAMP,
                    ExifInterface.TAG_MAKE,
                    ExifInterface.TAG_MODEL,
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.TAG_SUBSEC_TIME,
                    ExifInterface.TAG_WHITE_BALANCE
            };

    ExifInterface oldExif = new ExifInterface(originalPath);
    ExifInterface newExif = new ExifInterface(newPath);

    if (attributes.length > 0) {
        for (int i = 0; i < attributes.length; i++) {
            String value = oldExif.getAttribute(attributes[i]);
            if (value != null)
                newExif.setAttribute(attributes[i], value);
        }
        newExif.saveAttributes();
    }
}

readFile():

public static byte[] readFile(File file) throws IOException {
    // Open file
    RandomAccessFile f = new RandomAccessFile(file, "r");
    try {
        // Get and check length
        long longlength = f.length();
        int length = (int) longlength;
        if (length != longlength)
            throw new IOException("File size >= 2 GB");
        // Read file and return data
        byte[] data = new byte[length];
        f.readFully(data);
        return data;
    } finally {
        f.close();
    }
}
Ambran
  • 2,367
  • 4
  • 31
  • 46
2

The problem is that the image Exif data is lost when compressing the bitmap

The EXIF data is lost when reading in the Bitmap. A Bitmap has no EXIF tags.

How can Exif data be retained in the images when they'er uploaded to the server?

Stop reading in the Bitmap. Just upload the contents of postFilePath as-is. It will contain whatever EXIF tags it contains.

My assumption is that you are reading in the Bitmap in the hope that saving it again in 70% JPEG quality will result in meaningful bandwidth savings. I suspect that you are not saving very much, and you may be increasing the bandwidth in some cases (e.g., postFilePath points to a PNG). Your costs are a chunk of CPU time, an increased risk of an OutOfMemoryError, and the loss of your EXIF tags.

If, instead, the convert-to-70%-JPEG is some sort of data normalization approach, do that work on the server, where you have more CPU power, more disk space, more RAM, and continuous power.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • I see what you mean. As I said the code is 3-4 years old and have already been uploading millions of images to the server. The code scales the images before creating the bitmap, that's why they are not uploaded directly. I just didn't post the scaling code as it's long and I didn't think it was relevant. I'll have to look at the whole thing again. – Ambran Jul 05 '17 at 14:32
  • 1
    @Ambran: Ah, well, if you are also scaling the image, that can easily make a bigger difference in bandwidth consumption. In that case, though, rather than using the `ByteArrayOutputStream`, my guess is that you will need to write the scaled JPEG to a file, so you can re-apply the EXIF headers from the original file. Then, upload from that file. – CommonsWare Jul 05 '17 at 14:46
  • Yes, that's what @amuttsch also suggested so that's the way to go. I'm working on that now. – Ambran Jul 05 '17 at 14:51
0

Source: https://stackoverflow.com/a/11572752/8252521

Answered by: https://stackoverflow.com/users/1592398/code-jaff

Convert the file to bitmap by

Bitmap bi = BitmapFactory.decode(filepath + "DSC00021.jpg");

You can specify options too, look at API documentation

Or if you want to exchange the meta data from one file to another, sanselan will probably be the best choice. This would be much helpful when you manipulating the image, for example re-size.

The sample code will guide you in a right direction.

  • The link returns a 404, sanselan is now called Apache Commons Imaging, see https://commons.apache.org/proper/commons-imaging/ – amuttsch Jul 05 '17 at 14:21
0

You need to just create a new OutputStream to preserve the Exif Information. There is no need of creating a new File.