/*
* Copyright (C) 2014 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.builder.png;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.zip.CRC32;
/**
* A Png Chunk.
*
* A chunk contains a 4-byte length, a 4-byte type, a number of bytes for the payload, and finally
* a 4-byte CRC32.
*
* The length value is only the length of the payload. If there is no payload, it is 0, but
* the type and CRC32 are still there.
* Length is unsigned but max value is (2^31)-1.
*
* The CRC32 is computed on the type and payload but not length.
*
* Reference: http://tools.ietf.org/html/rfc2083#section-3.2
*
* The type of the chunk follows a very specific naming scheme.
* Reference: http://tools.ietf.org/html/rfc2083#section-3.3
*
*/
class Chunk {
/** Chunk type for the Image-Header chunk. */
public static final byte[] IHDR = new byte[] { 'I', 'H', 'D', 'R' };
@NonNull
private final byte[] mType;
@Nullable
private final byte[] mData;
private final long mCrc32;
@VisibleForTesting
Chunk(@NonNull byte[] type, @Nullable byte[] data, long crc32) {
checkNotNull(type);
checkArgument(type.length == 4);
mType = type;
mData = data;
mCrc32 = crc32;
}
Chunk(@NonNull byte[] type, @Nullable byte[] data) {
this(type, data, computeCrc32(type, data));
}
Chunk(@NonNull byte[] type) {
this(type, null);
}
/**
* Return the length info about the chunk. This is the length that
* is written in the PNG file.
*/
int getDataLength() {
return mData != null ? mData.length : 0;
}
/**
* returns the size of the chunk in the file.
*/
int size() {
// 4 bytes for each length, type and crc32.
// then add the data length.
return 12 + getDataLength();
}
@NonNull
byte[] getType() {
return mType;
}
String getTypeAsString() {
return new String(mType, Charsets.US_ASCII);
}
@Nullable
byte[] getData() {
return mData;
}
long getCrc32() {
return mCrc32;
}
private static long computeCrc32(@NonNull byte[] type, @Nullable byte[] data) {
CRC32 checksum = new CRC32();
checksum.update(type);
if (data != null) {
checksum.update(data);
}
return checksum.getValue();
}
void write(@NonNull OutputStream outStream) throws IOException {
ByteUtils utils = ByteUtils.Cache.get();
//write the length
outStream.write(utils.getIntAsArray(getDataLength()));
// write the type
outStream.write(mType);
// write the data if applicable
if (mData != null) {
outStream.write(mData);
}
// write the CRC32. This is a long converted to a 8 byte array,
// but we only care about the last 4 bytes.
outStream.write(utils.getLongAsIntArray(mCrc32), 4, 4);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Chunk chunk = (Chunk) o;
if (mCrc32 != chunk.mCrc32) {
return false;
}
if (!Arrays.equals(mData, chunk.mData)) {
return false;
}
if (!Arrays.equals(mType, chunk.mType)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = Arrays.hashCode(mType);
result = 31 * result + (mData != null ? Arrays.hashCode(mData) : 0);
result = 31 * result + (int) (mCrc32 ^ (mCrc32 >>> 32));
return result;
}
@Override
public String toString() {
if (Arrays.equals(mType, IHDR)) {
ByteBuffer buffer = ByteBuffer.wrap(mData);
return "Chunk{" +
"mType=" + getTypeAsString() +
", mData=" + buffer.getInt() + "x" + buffer.getInt() + ":" + buffer.get() +
"-" + buffer.get() + "-" + buffer.get() + "-" + buffer.get() +
"-" + buffer.get() +
'}';
}
return "Chunk{" +
"mType=" + getTypeAsString() +
(getDataLength() <= 200 ? ", mData=" + getArray() : "") +
", mData-Length=" + getDataLength() +
'}';
}
private String getArray() {
int len = getDataLength();
StringBuilder sb = new StringBuilder(len * 2);
if (mData != null) {
for (int i = 0 ; i < len ; i++) {
sb.append(String.format("%02X", mData[i]));
}
}
return sb.toString();
}
}