expo / expo

An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.
https://docs.expo.dev
MIT License
32.86k stars 5.23k forks source link

Image picker removes exif data from copied file #14568

Closed Dakuan closed 2 years ago

Dakuan commented 2 years ago

Summary

When you use ImagePicker.launchImageLibraryAsync it will extract the exif data from the image, and relay that if the {exif: true} option is provided. However, when the selected image is copied to the temp folder, the exif data is missing from the copied image. This means that if the image is uploaded, the image on the server will not have the exif data. This behaviour has not been requested, and shouldn't happen.

Managed or bare workflow? If you have ios/ or android/ directories in your project, the answer is bare!

managed

What platform(s) does this occur on?

iOS

SDK Version (managed workflow only)

42

Environment

  Expo CLI 4.9.0 environment info:
    System:
      OS: macOS 10.15.7
      Shell: 5.7.1 - /bin/zsh
    Binaries:
      Node: 16.5.0 - ~/.nvm/versions/node/v16.5.0/bin/node
      Yarn: 1.22.11 - ~/.nvm/versions/node/v16.5.0/bin/yarn
      npm: 7.20.2 - ~/.nvm/versions/node/v16.5.0/bin/npm
    SDKs:
      iOS SDK:
        Platforms: iOS 14.4, DriverKit 20.2, macOS 11.1, tvOS 14.3, watchOS 7.2
    IDEs:
      Android Studio: 4.2 AI-202.7660.26.42.7322048
      Xcode: 12.4/12D4e - /usr/bin/xcodebuild
    npmPackages:
      expo: ~42.0.0 => 42.0.3
      react: 16.13.1 => 16.13.1
      react-dom: 16.13.1 => 16.13.1
      react-native: https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz => 0.63.2
      react-native-web: ~0.13.12 => 0.13.18
    npmGlobalPackages:
      expo-cli: 4.9.0
    Expo Workflow: managed

Reproducible demo or steps to reproduce from a blank project

https://snack.expo.dev/@dakuan/exif-removed

Simek commented 2 years ago

Hi @Dakuan,

the exif-parser package you are using in the code outputs JSON in different format than the ImagePicker. The LensModel property you are looking for is located in tags not in exif after the parse.

You can see your example with the fix in there:

Dakuan commented 2 years ago

heya, thats not working for me:

image

same if I use the piexifjs lib:

Object {
  "0th": Object {
    "274": 1,
    "34665": 38,
  },
  "1st": Object {},
  "Exif": Object {
    "40961": 65535,
    "40962": 5472,
    "40963": 3648,
  },
  "GPS": Object {},
  "Interop": Object {},
  "thumbnail": null,
}
Simek commented 2 years ago

heya, thats not working for me:

Sorry, but I did not understand the problem, can you clarify? Is my Snack example not working for you, or you are talking about other code?

I'm not sure why you expect the different libraries to output exactly the same result. Currently for me it looks like the issues you are experiencing are not related to Expo.

Dakuan commented 2 years ago

Your updated snack is not retuning exif data for me. Same goes for when the file is eventually uploaded to cloud storage. its not reformatting, its removing the data entirely. It's for sure related to expo (or underlying lib). The photo 100% has exif data on device

Simek commented 2 years ago

Your updated snack is not retuning exif data for me.

This it weird, it might be related to the specific image you are trying to use. Snack works for me fine of the few test devices.

Also you need to remember that not all photos on the device will have the lens data, for example iOS screenshot do not have that property set at all, same for the most images download form the web.

Dakuan commented 2 years ago

Our app is for pro wildlife photographers. Their photos are straight from camera, or via adobe lightroom and contain lots of exif data. I've scraped all the files in the storage bucket, they are all blank. Images are not processed at all by our app. Just uploaded to GCloud.

before expo image

after expo: image

your snack: image

exif output from filesystem:


  "0th": Object {
    "274": 1,
    "34665": 38,
  },
  "1st": Object {},
  "Exif": Object {
    "40961": 65535,
    "40962": 5472,
    "40963": 3648,
  },
  "GPS": Object {},
  "Interop": Object {},
  "thumbnail": null,
}```
Simek commented 2 years ago

Still it looks like for me like exif-parser issue, since it's the library responsible for parsing the base64 string.

The ImagePicker returns correct EXIF (before value) as your example shows in the first place. It's one of your manipulation on that file which move/removes/inavlidates EXIF data.

Simek commented 2 years ago

Our app is for pro wildlife photographers. Their photos are straight from camera, or via adobe lightroom and contain lots of exif data. I've scraped all the files in the storage bucket, they are all blank. Images are not processed at all by our app. Just uploaded to GCloud.

Did you try to use uploadAsync (https://docs.expo.dev/versions/latest/sdk/filesystem/#filesystemuploadasyncurl-fileuri-options) in your case instead of using readAsStringAsync and re-parsing image back, just to send it?

And FYI you are processing the image right in here:

const file = await FileSystem.readAsStringAsync(result.uri, {
  encoding: FileSystem.EncodingType.Base64,
});
const buf = Buffer.from(file, "base64");
const parser = Exif.create(buf);
const res = await parser.parse();
Dakuan commented 2 years ago

I did, in the original app the images are not being messed with at all, fileuri's from the picker get passed around redux a bit and get uploaded to google cloud storage like this:

  const uploadResult = await FileSystem.uploadAsync(signedUrl, fileUri, {
    headers: {
      Accept: "application/json",
      "Content-Type": contentType,
    },
    httpMethod: "PUT",
  });

I have a script to inspect the files in the bucket:

import { Storage } from "@google-cloud/storage";
import * as piexif from "piexifjs";
import * as Exif from "exif-parser";

import fs from "fs";
import { promisify } from "util";
import path from "path";

const writeFile = promisify(fs.writeFile);

const BUCKET_NAME = "REDACTED";

async function stream2buffer(stream: Stream): Promise<Buffer> {
  return new Promise<Buffer>((resolve, reject) => {
    const _buf = Array<any>();

    stream.on("data", (chunk) => _buf.push(chunk));
    stream.on("end", () => resolve(Buffer.concat(_buf)));
    stream.on("error", (err) => reject(`error converting stream - ${err}`));
  });
}

(async function () {
  try {
    const storage = new Storage();

    const [files] = await storage.bucket(BUCKET_NAME).getFiles();

    console.log("Files:");

    await Promise.all(
      files.map(async (file) => {
        try {
          if (file.name.includes("users")) {
            const buffer = await stream2buffer(file.createReadStream());

            // const binaryString = buffer.toString("binary");
            // const existingExif = piexif.load(binaryString);

            const parser = Exif.create(buffer);
            const result = parser.parse();
            console.log(result);

            const segments = file.name.split("/");
            await writeFile(
              path.join(
                __dirname,
                "./results/parser/",
                `${segments[segments.length - 1]}.txt`
              ),
              JSON.stringify(result),
              { encoding: "utf-8" }
            );
          }
        } catch (e) {
          console.log(file.name, e);
        }
      })
    );
  } catch (err) {
    console.log(err);
    process.exit(1);
  }
})();

I've tried using the script with piexifjs and exif-parse npm modules, all blank:

image

{
   "startMarker":{
      "offset":0
   },
   "tags":{
      "Orientation":1,
      "ColorSpace":65535,
      "ExifImageWidth":1536,
      "ExifImageHeight":2048
   },
   "imageSize":{
      "height":2048,
      "width":1536
   },
   "app1Offset":24
}
github-actions[bot] commented 2 years ago

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

github-actions[bot] commented 2 years ago

This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem.

BrodaNoel commented 2 years ago

This issue is still reproducible