blob: ed8d7a6c003547e6d6f8b2cef1bedc50290fe355 [file] [edit]
/*
* Copyright (c) 2014-2021 by Wen Yu
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* or any later version.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
*
* Change History - most recent changes go on top of previous changes
*
* GIFMetq.java
*
* Who Date Description
* ==== ========= ==================================================
* WY 07Apr2016 Rewrite insertXMPApplicationBlock() to leverage GifXMP
* WY 16Sep2015 Added insertComment() to insert comment block
* WY 06Jul2015 Added insertXMP(InputSream, OutputStream, XMP)
* WY 30Mar2015 Fixed bug with insertXMP() replacing '\0' with ' '
* WY 13Mar2015 Initial creation
*/
package pixy.meta.gif;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Document;
import pixy.meta.Metadata;
import pixy.meta.MetadataType;
import pixy.meta.image.Comments;
import pixy.meta.xmp.XMP;
import pixy.io.IOUtils;
import pixy.string.XMLUtils;
import pixy.util.ArrayUtils;
/**
* GIF Metadata tool
*
* @author Wen Yu, yuwen_66@yahoo.com
* @version 1.0 04/16/2014
*/
public class GIFMeta {
// Define constants
public static final byte IMAGE_SEPARATOR = 0x2c; // ","
public static final byte IMAGE_TRAILER = 0x3b; // ";"
public static final byte EXTENSION_INTRODUCER = 0x21; // "!"
public static final byte GRAPHIC_CONTROL_LABEL = (byte)0xf9;
public static final byte APPLICATION_EXTENSION_LABEL = (byte)0xff;
public static final byte COMMENT_EXTENSION_LABEL = (byte)0xfe;
public static final byte TEXT_EXTENSION_LABEL = 0x01;
public static final int DISPOSAL_UNSPECIFIED = 0;
public static final int DISPOSAL_LEAVE_AS_IS = 1;
public static final int DISPOSAL_RESTORE_TO_BACKGROUND = 2;
public static final int DISPOSAL_RESTORE_TO_PREVIOUS = 3;
// Data transfer object for multiple thread support
private static class DataTransferObject {
private byte[] header;
private byte[] logicalScreenDescriptor;
private byte[] globalPalette;
private byte[] imageDescriptor;
private Map<MetadataType, Metadata> metadataMap;
private Comments comments;
}
public static void insertComments(InputStream is, OutputStream os, List<String> comments) throws IOException {
// Read and copy header and LSD
// Create a new data transfer object to hold data
DataTransferObject DTO = new DataTransferObject();
readHeader(is, DTO);
readLSD(is, DTO);
os.write(DTO.header);
os.write(DTO.logicalScreenDescriptor);
if((DTO.logicalScreenDescriptor[4]&0x80) == 0x80) {
int bitsPerPixel = (DTO.logicalScreenDescriptor[4]&0x07)+1;
int colorsUsed = (1 << bitsPerPixel);
readGlobalPalette(is, colorsUsed, DTO);
os.write(DTO.globalPalette);
}
int numOfComments = comments.size();
for(int i = 0; i < numOfComments; i++) {
os.write(EXTENSION_INTRODUCER);
os.write(COMMENT_EXTENSION_LABEL);
byte[] commentBytes = comments.get(i).getBytes();
int numBlocks = commentBytes.length/0xff;
int leftOver = commentBytes.length % 0xff;
int offset = 0;
if(numBlocks > 0) {
for(int block = 0; block < numBlocks; block++) {
os.write(0xff);
os.write(commentBytes, offset, 0xff);
offset += 0xff;
}
}
if(leftOver > 0) {
os.write(leftOver);
os.write(commentBytes, offset, leftOver);
}
os.write(0);
}
// Copy the rest of the input stream
byte buf[] = new byte[10240]; // 10K
int bytesRead = is.read(buf);
while(bytesRead != -1) {
os.write(buf, 0, bytesRead);
bytesRead = is.read(buf);
}
}
public static void insertXMPApplicationBlock(InputStream is, OutputStream os, XMP xmp) throws IOException {
insertXMPApplicationBlock(is, os, xmp.getData());
}
public static void insertXMPApplicationBlock(InputStream is, OutputStream os, byte[] xmp) throws IOException {
byte[] buf = new byte[14];
buf[0] = EXTENSION_INTRODUCER; // Extension introducer
buf[1] = APPLICATION_EXTENSION_LABEL; // Application extension label
buf[2] = 0x0b; // Block size
buf[3] = 'X'; // Application Identifier (8 bytes)
buf[4] = 'M';
buf[5] = 'P';
buf[6] = ' ';
buf[7] = 'D';
buf[8] = 'a';
buf[9] = 't';
buf[10]= 'a';
buf[11]= 'X';// Application Authentication Code (3 bytes)
buf[12]= 'M';
buf[13]= 'P';
// Create a byte array from 0x01, 0xFF - 0x00, 0x00
byte[] magic_trailer = new byte[258];
magic_trailer[0] = 0x01;
for(int i = 255; i >= 0; i--)
magic_trailer[256 - i] = (byte)i;
// Read and copy header and LSD
// Create a new data transfer object to hold data
DataTransferObject DTO = new DataTransferObject();
readHeader(is, DTO);
readLSD(is, DTO);
os.write(DTO.header);
os.write(DTO.logicalScreenDescriptor);
if((DTO.logicalScreenDescriptor[4]&0x80) == 0x80) {
int bitsPerPixel = (DTO.logicalScreenDescriptor[4]&0x07)+1;
int colorsUsed = (1 << bitsPerPixel);
readGlobalPalette(is, colorsUsed, DTO);
os.write(DTO.globalPalette);
}
// Insert XMP here
// Write extension introducer and application identifier
os.write(buf);
// Write the XMP packet
os.write(xmp);
// Write the magic trailer
os.write(magic_trailer);
// End of XMP data
// Copy the rest of the input stream
buf = new byte[10240]; // 10K
int bytesRead = is.read(buf);
while(bytesRead != -1) {
os.write(buf, 0, bytesRead);
bytesRead = is.read(buf);
}
}
public static void insertXMPApplicationBlock(InputStream is, OutputStream os, String xmp) throws IOException {
Document doc = XMLUtils.createXML(xmp);
XMLUtils.insertLeadingPI(doc, "xpacket", "begin='' id='W5M0MpCehiHzreSzNTczkc9d'");
XMLUtils.insertTrailingPI(doc, "xpacket", "end='w'");
// Serialize doc to byte array
byte[] xmpBytes = XMLUtils.serializeToByteArray(doc);
insertXMPApplicationBlock(is, os, xmpBytes);
}
private static boolean readFrame(InputStream is, DataTransferObject DTO) throws IOException {
// Need to reset some of the fields
int disposalMethod = -1;
// End of fields reset
int image_separator = 0;
do {
image_separator = is.read();
if(image_separator == -1 || image_separator == 0x3b) { // End of stream
return false;
}
if (image_separator == 0x21) { // (!) Extension Block
int func = is.read();
int len = is.read();
if (func == 0xf9) {
// Graphic Control Label - identifies the current block as a Graphic Control Extension
//<<Start of graphic control block>>
int packedFields = is.read();
// Determine the disposal method
disposalMethod = ((packedFields&0x1c)>>2);
switch(disposalMethod) {
case DISPOSAL_UNSPECIFIED:
// Frame disposal method: UNSPECIFIED
case DISPOSAL_LEAVE_AS_IS:
// Frame disposal method: LEAVE_AS_IS
case DISPOSAL_RESTORE_TO_BACKGROUND:
// Frame disposal method: RESTORE_TO_BACKGROUND
case DISPOSAL_RESTORE_TO_PREVIOUS:
// Frame disposal method: RESTORE_TO_PREVIOUS
break;
default:
//throw new RuntimeException("Invalid GIF frame disposal method: " + disposalMethod);
}
// Check for transparent color flag
if((packedFields&0x01) == 0x01) {
IOUtils.skipFully(is, 2);
// Transparent GIF
is.read(); // Transparent color index
len = is.read();// len=0, block terminator!
} else {
IOUtils.skipFully(is, 3);
len = is.read();// len=0, block terminator!
}
// <<End of graphic control block>>
} else if(func == 0xff) { // Application block
// Application block
byte[] xmp_id = {'X', 'M', 'P', ' ', 'D', 'a', 't', 'a', 'X', 'M', 'P' };
byte[] temp = new byte[0x0B];
IOUtils.readFully(is, temp);
// If we have XMP data
if(Arrays.equals(xmp_id, temp)) {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
len = is.read();
while(len != 0) {
bout.write(len);
temp = new byte[len];
IOUtils.readFully(is, temp);
bout.write(temp);
len = is.read();
}
byte[] xmp = bout.toByteArray();
// Remove the magic trailer - 258 bytes minus the block terminator
len = xmp.length - 257;
if(len > 0) // Put it into the Meta data map
DTO.metadataMap.put(MetadataType.XMP, new GifXMP(ArrayUtils.subArray(xmp, 0, len)));
len = 0; // We're already at block terminator
} else
len = is.read(); // Block terminator
} else if(func == 0xfe) { // Comment block
// Comment block
byte[] comment = new byte[len];
IOUtils.readFully(is, comment);
if(DTO.comments == null) DTO.comments = new Comments();
DTO.comments.addComment(comment);
// Comment: new String(comment)
len = is.read();
}
// GIF87a specification mentions the repetition of multiple length
// blocks while GIF89a gives no specific description. For safety, here
// a while loop is used to check for block terminator!
while(len != 0) {
IOUtils.skipFully(is, len);
len = is.read();// len=0, block terminator!
}
}
} while(image_separator != 0x2c); // ","
// <<Start of new frame>>
readImageDescriptor(is, DTO);
int colorsUsed = 1 << ((DTO.logicalScreenDescriptor[4]&0x07)+1);
byte[] localPalette = null;
if((DTO.imageDescriptor[8]&0x80) == 0x80) {
// A local color map is present
int bitsPerPixel = (DTO.imageDescriptor[8]&0x07)+1;
// Colors used in local palette
colorsUsed = (1<<bitsPerPixel);
localPalette = new byte[3*colorsUsed];
is.read(localPalette);
}
if(localPalette == null) localPalette = DTO.globalPalette;
is.read(); // LZW Minimum Code Size
int len = 0;
while((len = is.read()) > 0) {
byte[] block = new byte[len];
is.read(block);
}
return true;
}
private static void readGlobalPalette(InputStream is, int num_of_color, DataTransferObject DTO) throws IOException {
DTO.globalPalette = new byte[num_of_color*3];
is.read(DTO.globalPalette);
}
private static void readHeader(InputStream is, DataTransferObject DTO) throws IOException {
DTO.header = new byte[6]; // GIFXXa
is.read(DTO.header);
}
private static void readImageDescriptor(InputStream is, DataTransferObject DTO) throws IOException {
DTO.imageDescriptor = new byte[9];
is.read(DTO.imageDescriptor);
}
private static void readLSD(InputStream is, DataTransferObject DTO) throws IOException {
DTO.logicalScreenDescriptor = new byte[7];
is.read(DTO.logicalScreenDescriptor);
}
public static Map<MetadataType, Metadata> readMetadata(InputStream is) throws IOException {
// Create a new data transfer object to hold data
DataTransferObject DTO = new DataTransferObject();
// Created a Map for the Meta data
DTO.metadataMap = new HashMap<MetadataType, Metadata>();
readHeader(is, DTO);
readLSD(is, DTO);
// Packed byte
if((DTO.logicalScreenDescriptor[4]&0x80) == 0x80) {
// A global color map is present
int bitsPerPixel = (DTO.logicalScreenDescriptor[4]&0x07)+1;
int colorsUsed = (1 << bitsPerPixel);
readGlobalPalette(is, colorsUsed, DTO);
}
while(readFrame(is, DTO)) {
;
}
if(DTO.comments != null)
DTO.metadataMap.put(MetadataType.COMMENT, DTO.comments);
return DTO.metadataMap;
}
private GIFMeta() {}
}