All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.images.ImageImpl Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.google.appengine.api.images;

import com.google.appengine.api.blobstore.BlobKey;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * Implementation of the {@link Image} interface.
 *
 */
final class ImageImpl implements Image {
  static final long serialVersionUID = -7970719108027515279L;

  private byte[] imageData;
  private int width;
  private int height;
  private @Nullable Format format;
  private @Nullable BlobKey blobKey;

  private static final int EOI_MARKER = 0xd9;

  private static final int RST_MARKER_START = 0xd0;

  private static final int RST_MARKER_END = 0xd7;

  private static final int TEM_MARKER = 0x01;

  private static final int HUFFMAN_TABLE_MARKER = 0xc4;

  private static final int ARITHMETIC_CODING_CONDITIONING_MARKER = 0xcc;

  /**
   * Creates an image containing the supplied image data
   * @param imageData contents of the image to be created
   * @throws IllegalArgumentException If {@code imageData} is null or empty.
  */
  ImageImpl(byte[] imageData) {
    setImageData(imageData);
  }

  ImageImpl(BlobKey blobKey) {
    this.width = -1;
    this.height = -1;
    this.blobKey = blobKey;
  }

  /** {@inheritDoc} */
  @Override
  public int getWidth() {
    if (width < 0) {
      updateDimensions();
    }
    return width;
  }

  /** {@inheritDoc} */
  @Override
  public int getHeight() {
    if (height < 0) {
      updateDimensions();
    }
    return height;
  }

  /** {@inheritDoc} */
  @Override
  public @Nullable Format getFormat() {
    if (format == null) {
      updateDimensions();
    }
    return format;
  }

  /** {@inheritDoc} */
  @Override
  public byte @Nullable [] getImageData() {
    return (imageData != null) ? imageData.clone() : null;
  }

  /** {@inheritDoc} */
  @Override
  public void setImageData(byte[] imageData) {
    if (imageData == null) {
      throw new IllegalArgumentException("imageData must not be null");
    }
    if (imageData.length == 0) {
      throw new IllegalArgumentException("imageData must not be empty");
    }
    this.imageData = imageData.clone();
    // Using -1 to represent uninitialised values for lazy initialisation.
    this.width = -1;
    this.height = -1;
    this.format = null;
    this.blobKey = null;
  }

  @Override
  public @Nullable BlobKey getBlobKey() {
    return blobKey;
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (o instanceof Image) {
      Image other = (Image) o;
      BlobKey otherBlobKey = other.getBlobKey();
      if (blobKey != null || otherBlobKey != null) {
        return Objects.equals(blobKey, otherBlobKey);
      } else {
        return Arrays.equals(imageData, other.getImageData());
      }
    }
    return false;
  }

  @Override
  public int hashCode() {
    if (blobKey != null) {
      return blobKey.hashCode();
    } else {
      return Arrays.hashCode(imageData);
    }
  }

  /**
   * Updates the dimension fields of the image.
   *
   */
  private void updateDimensions() {
    if (imageData == null) {
      throw new UnsupportedOperationException("No image data is available.");
    }
    if (imageData.length < 8) {
      throw new IllegalArgumentException("imageData must be a valid image");
    }
    // Check for magic numbers and send off to the appropriate format function.
    if (imageData[0] == 'G' && imageData[1] == 'I' && imageData[2] == 'F') {
      updateGifDimensions();
      format = Format.GIF;
    } else if (imageData[0] == (byte) 0x89 && imageData[1] == 'P'
               && imageData[2] == 'N' && imageData[3] == 'G'
               && imageData[4] == 0x0d && imageData[5] == 0x0a
               && imageData[6] == 0x1a && imageData[7] == 0x0a) {
      updatePngDimensions();
      format = Format.PNG;
    } else if (imageData[0] == (byte) 0xff && imageData[1] == (byte) 0xd8) {
      updateJpegDimensions();
      format = Format.JPEG;
    } else if ((imageData[0] == 'I' && imageData[1] == 'I'
                && imageData[2] == 0x2a && imageData[3] == 0x00)
               || (imageData[0] == 'M' && imageData[1] == 'M'
                   && imageData[2] == 0x00 && imageData[3] == 0x2a)) {
      updateTiffDimensions();
      format = Format.TIFF;
    } else if (imageData[0] == 'B' && imageData[1] == 'M') {
      updateBmpDimensions();
      format = Format.BMP;
    } else if (imageData[0] == 0x00 && imageData[1] == 0x00
               && imageData[2] == 0x01 && imageData[3] == 0x00) {
      updateIcoDimensions();
      format = Format.ICO;
    } else if (imageData.length > 16
               && imageData[0] == 'R' && imageData[1] == 'I'
               && imageData[2] == 'F' && imageData[3] == 'F'
               && imageData[8] == 'W' && imageData[9] == 'E'
               && imageData[10] == 'B' && imageData[11] == 'P') {
      if (imageData[12] == 'V' && imageData[13] == 'P'
          && imageData[14] == '8' && imageData[15] == ' ') {
        updateWebpDimensions();
        format = Format.WEBP;
      } else if (imageData[12] == 'V' && imageData[13] == 'P'
                 && imageData[14] == '8' && imageData[15] == 'X') {
        updateWebpVp8xDimensions();
        format = Format.WEBP;
      } else {
        throw new IllegalArgumentException("imageData must be a valid image");
      }
    } else {
      throw new IllegalArgumentException("imageData must be a valid image");
    }
  }

  /**
   * Updates the dimension fields of the GIF image.
   * Based on http://www.w3.org/Graphics/GIF/spec-gif89a.txt.
   *
   */
  private void updateGifDimensions() {
    if (imageData.length < 10) {
      throw new IllegalArgumentException("corrupt GIF format");
    }
    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    width = buffer.getChar(6) & 0xffff;
    height = buffer.getChar(8) & 0xffff;
  }

  /**
   * Updates the dimension fields of the PNG image.
   * Based on http://www.w3.org/TR/2003/REC-PNG-20031110/ sections 5 and
   * 11.2.2.
   *
   */
  private void updatePngDimensions() {
    if (imageData.length < 24) {
      throw new IllegalArgumentException("corrupt PNG format");
    }
    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.BIG_ENDIAN);
    width = buffer.getInt(16);
    height = buffer.getInt(20);
  }

  /**
   * Updates the dimension fields of the JPEG image.
   * Based on http://www.w3.org/Graphics/JPEG/itu-t81.pdf.
   *
   */
  private void updateJpegDimensions() {

    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.BIG_ENDIAN);

    // Check for SOI marker - Must be first marker in a JPG.
    if (extend(buffer.get()) != 0xff || extend(buffer.get()) != 0xd8) {
      throw new IllegalArgumentException("corrupt JPEG format: Expected SOI marker");
    }

    int code;

    try {
      while (true) {
        // Search for next marker
        // NB I don't believe this loop is technically necessary.
        do {
          code = extend(buffer.get());
        } while (code != 0xff);

        // Markers may be  preceded by an indefinite number of "fill" bytes.
        while (code == 0xff) {
          code = extend(buffer.get());
        }

        if (isFrameMarker(code)) {
          // If the marker is any kind of frame marker, it contains the
          // size of the image, but first we need to skip over the frame
          // length (2 bytes) and the precision (1 byte).
          buffer.position(buffer.position() + 3);
          height = extend(buffer.getShort());
          width = extend(buffer.getShort());
          return;
        }

        // If it's not a frame marker, we want to skip over it.
        // The markers SOI, EOI, RST, and TEM have no segments and are not
        // followed by a length. We don't need to worry about SOI - it's the
        // first marker and only shows up once.
        if (code == EOI_MARKER) {
          throw new IllegalArgumentException("corrupt JPEG format: No frame sgements found.");
        }

        if (code >= RST_MARKER_START && code <= RST_MARKER_END) {
          continue;
        }

        if (code == TEM_MARKER) {
          continue;
        }

        // All other markers are followed immediately by the length of their
        // segment. The value of the length includes the two bytes of the
        // length itself, but does not include the two bytes of the preceding
        // marker.
        int length = extend(buffer.getShort(buffer.position()));
        buffer.position(buffer.position() + length);
      }
    } catch (IllegalArgumentException ex) {
      throw new IllegalArgumentException("corrupt JPEG format");
    } catch (BufferUnderflowException ex) {
      throw new IllegalArgumentException("corrupt JPEG format");
    }
  }

  private static boolean isFrameMarker(int code) {
    // All frame markers have their high order nibble set to 0xC0
    // except for HUFFMAN and ACC.
    return ((code & 0xf0) == 0xc0)
        && code != HUFFMAN_TABLE_MARKER
        && code != ARITHMETIC_CODING_CONDITIONING_MARKER;
  }

  private static int extend(byte b) {
    return b & 0xFF;
  }

  private static int extend(short s) {
    return s & 0xFFFF;
  }

  /**
   * Updates the dimension fields of the TIFF image.
   * Based on http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
   * sections 2 and 3.
   *
   */
  private void updateTiffDimensions() {
    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    if (imageData[0] == 'I') {
      buffer.order(ByteOrder.LITTLE_ENDIAN);
    }
    int offset = buffer.getInt(4);

    int ifdSize = buffer.getChar(offset) & 0xffff;
    offset += 2;
    for (int i = 0; i < ifdSize && offset + 12 <= imageData.length; i++) {
      int tag = buffer.getChar(offset) & 0xffff;
      if (tag == 0x100 || tag == 0x101) {
        int type = buffer.getChar(offset + 2) & 0xffff;
        int result;
        if (type == 3) {
          result = buffer.getChar(offset + 8) & 0xffff;
        } else if (type == 4) {
          result = buffer.getInt(offset + 8);
        } else {
          result = imageData[offset + 8];
        }
        if (tag == 0x100) {
          width = result;
          if (height != -1) {
            return;
          }
        } else {
          height = result;
          if (width != -1) {
            return;
          }
        }
      }
      offset += 12;
    }
    if (width == -1 || height == -1) {
      throw new IllegalArgumentException("corrupt tiff format");
    }
  }

  /**
   * Updates the dimension fields of the BMP image.
   * Based on http://msdn.microsoft.com/en-us/library/ms532290(VS.85).aspx
   * http://msdn.microsoft.com/en-us/library/ms532300(VS.85).aspx
   * http://msdn.microsoft.com/en-us/library/ms532331(VS.85).aspx
   * for windows versions.
   *
   */
  private void updateBmpDimensions() {
    if (imageData.length < 18) {
      throw new IllegalArgumentException("corrupt BMP format");
    }
    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    width = buffer.get(6) & 0xff;
    height = buffer.get(7) & 0xff;
    int headerLength = buffer.getInt(14);
    if (headerLength == 12 && imageData.length >= 22) {
      width = buffer.getChar(18) & 0xffff;
      height = buffer.getChar(20) & 0xffff;
    } else if ((headerLength == 40 || headerLength == 108
                || headerLength == 124 || headerLength == 64)
               && imageData.length >= 26) {
      width = buffer.getInt(18);
      height = buffer.getInt(22);
    } else {
      throw new IllegalArgumentException("corrupt BMP format");
    }
  }

  /**
   * Updates the dimension fields of the ICO image.
   *
   */
  private void updateIcoDimensions() {
    if (imageData.length < 8) {
      throw new IllegalArgumentException("corrupt ICO format");
    }
    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    width = buffer.get(6) & 0xff;
    height = buffer.get(7) & 0xff;

    // 256 is stored as 0.
    if (width == 0) {
      width = 256;
    }
    if (height == 0) {
      height = 256;
    }
  }

  private void updateWebpDimensions() {
    if (imageData.length < 30) {
      throw new IllegalArgumentException("corrupt WEBP format");
    }

    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);

    int bits = buffer.get(20) | buffer.get(21) << 8 | buffer.get(22) << 16;

    boolean keyFrame = (bits & 1) == 0;

    if (!keyFrame) {
      throw new IllegalArgumentException("corrupt WEBP format: not a key frame");
    }

    int profile = (bits >> 1) & 7;
    int showFrame = (bits >> 4) & 1;

    if (profile > 3) {
      throw new IllegalArgumentException("corrupt WEBP format: invalid profile");
    }
    if (showFrame == 0) {
      throw new IllegalArgumentException("corrupt WEBP format: frame is not visible");
    }

    width = extend(buffer.getShort(26));
    height = extend(buffer.getShort(28));
  }

  private void updateWebpVp8xDimensions() {
    if (imageData.length < 32) {
      throw new IllegalArgumentException("corrupt WEBP format");
    }

    ByteBuffer buffer = ByteBuffer.wrap(imageData);
    buffer.order(ByteOrder.LITTLE_ENDIAN);

    width = buffer.getInt(24);
    height = buffer.getInt(28);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy