/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.imageutils; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import android.graphics.Rect; import com.facebook.common.internal.Preconditions; import com.facebook.common.logging.FLog; /** * Util for obtaining information from JPEG file. */ public class JfifUtil { private static final Class<?> TAG = JfifUtil.class; /** * Definitions of jpeg markers as well as overall description of jpeg file format can be found * here: <a href="http://www.w3.org/Graphics/JPEG/itu-t81.pdf">Recommendation T.81</a> */ public static final int MARKER_FIRST_BYTE = 0xFF; public static final int MARKER_ESCAPE_BYTE = 0x00; public static final int MARKER_SOI = 0xD8; public static final int MARKER_TEM = 0x01; public static final int MARKER_EOI = 0xD9; public static final int MARKER_SOS = 0xDA; public static final int MARKER_APP1 = 0xE1; public static final int MARKER_SOFn = 0xC0; public static final int MARKER_RST0 = 0xD0; public static final int MARKER_RST7 = 0xD7; public static final int APP1_EXIF_MAGIC = 0x45786966; private JfifUtil() { } /** * Determines auto-rotate angle based on orientation information. * @param orientation orientation information, one of {1, 3, 6, 8}. * @return orientation: 1/3/6/8 -> 0/180/90/270. */ public static int getAutoRotateAngleFromOrientation(int orientation) { return TiffUtil.getAutoRotateAngleFromOrientation(orientation); } /** * Gets orientation information from jpeg byte array. * @param jpeg the input byte array of jpeg image * @return orientation: 1/8/3/6. Returns 0 if there is no valid orientation information. */ public static int getOrientation(byte[] jpeg) { // wrapping with ByteArrayInputStream is cheap and we don't have duplicate implementation return getOrientation(new ByteArrayInputStream(jpeg)); } /** * Get orientation information from jpeg input stream. * @param is the input stream of jpeg image * @return orientation: 1/8/3/6. Returns 0 if there is no valid orientation information. */ public static int getOrientation(InputStream is) { try { int length = moveToAPP1EXIF(is); if (length == 0) { return 0; // unknown orientation } return TiffUtil.readOrientationFromTIFF(is, length); } catch (IOException ioe) { return 0; } } /** * Get image width and height from jpeg header * @param jpeg the input byte array of jpeg image * @return dimensions of the image in form of Rect. */ public static Rect getDimensions(byte[] jpeg) { // wrapping with ByteArrayInputStream is cheap and we don't have duplicate implementation return getDimensions(new ByteArrayInputStream(jpeg)); } /** * Get image width and height from jpeg header * @param is the input stream of jpeg image * @return dimensions of the image in form of Rect */ public static Rect getDimensions(InputStream is) { try { if (moveToMarker(is, MARKER_SOFn)) { // read block length // subtract 2 as length contain SIZE field we just read int length = StreamProcessor.readPackedInt(is, 2, false) - 2; if (length > 6) { // SOFn structure: 0xFFCn|length(2)|bitDepth(1)|height(2)|width(2)|... int bitDepth = StreamProcessor.readPackedInt(is, 1, false); int height = StreamProcessor.readPackedInt(is, 2, false); int width = StreamProcessor.readPackedInt(is, 2, false); return new Rect(0, 0, width, height); } } } catch (IOException ioe) { FLog.e(TAG, ioe, "%x: getDimensions", is.hashCode()); // log and return null. } return null; } /** * Reads the content of the input stream until specified marker is found. Marker will be * consumed and the input stream will be positioned after the specified marker. * @param is the input stream to read bytes from * @param markerToFind the marker we are looking for * @return boolean: whether or not we found the expected marker from input stream. */ public static boolean moveToMarker(InputStream is, int markerToFind) throws IOException { Preconditions.checkNotNull(is); // ISO/IEC 10918-1:1993(E) while (StreamProcessor.readPackedInt(is, 1, false) == MARKER_FIRST_BYTE) { int marker = MARKER_FIRST_BYTE; while (marker == MARKER_FIRST_BYTE) { marker = StreamProcessor.readPackedInt(is, 1, false); } if (markerToFind == MARKER_SOFn && isSOFn(marker)) { return true; } if (marker == markerToFind) { return true; } // Check if the marker is SOI or TEM. These two don't have length field, so we skip it. if (marker == MARKER_SOI || marker == MARKER_TEM) { continue; } // Check if the marker is EOI or SOS. We will stop reading since metadata markers don't // come after these two markers. if (marker == MARKER_EOI || marker == MARKER_SOS) { return false; } // read block length // subtract 2 as length contain SIZE field we just read int length = StreamProcessor.readPackedInt(is, 2, false) - 2; // Skip other markers. is.skip(length); } return false; } private static boolean isSOFn(int marker) { // There are no SOF4, SOF8, SOF12 switch (marker) { case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC5: case 0xC6: case 0xC7: case 0xC9: case 0xCA: case 0xCB: case 0xCD: case 0xCE: case 0xCF: return true; default: return false; } } /** * Positions the given input stream to the beginning of the EXIF data in the JPEG APP1 block. * @param is the input stream of jpeg image * @return length of EXIF data */ private static int moveToAPP1EXIF(InputStream is) throws IOException { if (moveToMarker(is, MARKER_APP1)) { // read block length // subtract 2 as length contain SIZE field we just read int length = StreamProcessor.readPackedInt(is, 2, false) - 2; if (length > 6) { int magic = StreamProcessor.readPackedInt(is, 4, false); length -= 4; int zero = StreamProcessor.readPackedInt(is, 2, false); length -= 2; if (magic == APP1_EXIF_MAGIC && zero == 0) { // JEITA CP-3451 Exif Version 2.2 return length; } } } return 0; } }