/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.tika.parser.image; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.apache.poi.util.IOUtils; import org.apache.tika.exception.TikaException; import org.apache.tika.io.EndianUtils; import org.apache.tika.metadata.Metadata; import org.apache.tika.metadata.Photoshop; import org.apache.tika.metadata.TIFF; import org.apache.tika.metadata.TikaCoreProperties; import org.apache.tika.mime.MediaType; import org.apache.tika.parser.AbstractParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.sax.XHTMLContentHandler; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import static java.nio.charset.StandardCharsets.US_ASCII; /** * Parser for the Adobe Photoshop PSD File Format. * <p/> * Documentation on the file format is available from * http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/PhotoshopFileFormats.htm */ public class PSDParser extends AbstractParser { /** * Serial version UID */ private static final long serialVersionUID = 883387734607994914L; private static final Set<MediaType> SUPPORTED_TYPES = Collections.unmodifiableSet(new HashSet<MediaType>(Arrays.asList( MediaType.image("vnd.adobe.photoshop")))); public Set<MediaType> getSupportedTypes(ParseContext context) { return SUPPORTED_TYPES; } public void parse( InputStream stream, ContentHandler handler, Metadata metadata, ParseContext context) throws IOException, SAXException, TikaException { // Check for the magic header signature byte[] signature = new byte[4]; IOUtils.readFully(stream, signature); if (signature[0] == (byte) '8' && signature[1] == (byte) 'B' && signature[2] == (byte) 'P' && signature[3] == (byte) 'S') { // Good, signature found } else { throw new TikaException("PSD/PSB magic signature invalid"); } // Check the version int version = EndianUtils.readUShortBE(stream); if (version == 1 || version == 2) { // Good, we support these two } else { throw new TikaException("Invalid PSD/PSB version " + version); } // Skip the reserved block IOUtils.readFully(stream, new byte[6]); // Number of channels in the image int numChannels = EndianUtils.readUShortBE(stream); // TODO Identify a suitable metadata key for this // Width and Height int height = EndianUtils.readIntBE(stream); int width = EndianUtils.readIntBE(stream); metadata.set(TIFF.IMAGE_LENGTH, height); metadata.set(TIFF.IMAGE_WIDTH, width); // Depth (bits per channel) int depth = EndianUtils.readUShortBE(stream); metadata.set(TIFF.BITS_PER_SAMPLE, Integer.toString(depth)); // Colour mode, eg Bitmap or RGB int colorMode = EndianUtils.readUShortBE(stream); metadata.set(Photoshop.COLOR_MODE, Photoshop._COLOR_MODE_CHOICES_INDEXED[colorMode]); // Next is the Color Mode section // We don't care about this bit long colorModeSectionSize = EndianUtils.readIntBE(stream); stream.skip(colorModeSectionSize); // Next is the Image Resources section // Check for certain interesting keys here long imageResourcesSectionSize = EndianUtils.readIntBE(stream); long read = 0; while (read < imageResourcesSectionSize) { ResourceBlock rb = new ResourceBlock(stream); read += rb.totalLength; // Is it one we can do something useful with? if (rb.id == ResourceBlock.ID_CAPTION) { metadata.add(TikaCoreProperties.DESCRIPTION, rb.getDataAsString()); } else if (rb.id == ResourceBlock.ID_EXIF_1) { // TODO Parse the EXIF info via ImageMetadataExtractor } else if (rb.id == ResourceBlock.ID_EXIF_3) { // TODO Parse the EXIF info via ImageMetadataExtractor } else if (rb.id == ResourceBlock.ID_XMP) { // TODO Parse the XMP info via ImageMetadataExtractor } } // Next is the Layer and Mask Info // Finally we have Image Data // We can't do anything with these parts // We don't have any helpful text, sorry... XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata); xhtml.startDocument(); xhtml.endDocument(); } private static class ResourceBlock { private static final long SIGNATURE = 0x3842494d; // 8BIM private static final int ID_CAPTION = 0x03F0; private static final int ID_URL = 0x040B; private static final int ID_EXIF_1 = 0x0422; private static final int ID_EXIF_3 = 0x0423; private static final int ID_XMP = 0x0424; private int id; private String name; private byte[] data; private int totalLength; private ResourceBlock(InputStream stream) throws IOException, TikaException { // Verify the signature long sig = EndianUtils.readIntBE(stream); if (sig != SIGNATURE) { throw new TikaException("Invalid Image Resource Block Signature Found, got " + sig + " 0x" + Long.toHexString(sig) + " but the spec defines " + SIGNATURE); } // Read the block id = EndianUtils.readUShortBE(stream); StringBuffer nameB = new StringBuffer(); int nameLen = 0; while (true) { int v = stream.read(); nameLen++; if (v == 0) { // The name length is padded to be even if (nameLen % 2 == 1) { stream.read(); nameLen++; } break; } else { nameB.append((char) v); } name = nameB.toString(); } int dataLen = EndianUtils.readIntBE(stream); if (dataLen % 2 == 1) { // Data Length is even padded dataLen = dataLen + 1; } totalLength = 4 + 2 + nameLen + 4 + dataLen; data = new byte[dataLen]; IOUtils.readFully(stream, data); } private String getDataAsString() { // Will be null padded return new String(data, 0, data.length - 1, US_ASCII); } } }