001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.png.chunks;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.nio.charset.StandardCharsets;
022import java.util.zip.InflaterInputStream;
023
024import org.apache.commons.imaging.ImagingException;
025import org.apache.commons.imaging.common.Allocator;
026import org.apache.commons.imaging.common.BinaryFunctions;
027import org.apache.commons.imaging.formats.png.AbstractPngText;
028import org.apache.commons.imaging.formats.png.PngConstants;
029import org.apache.commons.io.IOUtils;
030
031public final class PngChunkItxt extends AbstractPngTextChunk {
032
033    private final String keyword;
034    private final String text;
035
036    /**
037     * The language tag defined in [RFC-3066] indicates the human language used by the translated keyword and the text. Unlike the keyword, the language tag is
038     * case-insensitive. It is an ISO 646.IRV:1991 [ISO 646] string consisting of hyphen-separated words of 1-8 alphanumeric characters each (for example cn,
039     * en-uk, no-bok, x-klingon, x-KlInGoN). If the first word is two or three letters long, it is an ISO language code [ISO-639]. If the language tag is empty,
040     * the language is unspecified.
041     */
042    private final String languageTag;
043
044    private final String translatedKeyword;
045
046    /**
047     * Constructs a new instance.
048     *
049     * @param length    chunk length.
050     * @param chunkType chunk type.
051     * @param crc       CRC computed over the chunk type and chunk data (but not the length).
052     * @param bytes     chunk data bytes.
053     * @throws ImagingException Thrown on a parsing error.
054     * @throws IOException Thrown on reading error.
055     * @throws NullPointerException if {@code bytes} is null.
056     */
057    public PngChunkItxt(final int length, final int chunkType, final int crc, final byte[] bytes) throws ImagingException, IOException {
058        super(length, chunkType, crc, bytes);
059        int terminator = BinaryFunctions.indexOf0(bytes, "PNG iTXt chunk keyword is not terminated.");
060
061        keyword = new String(bytes, 0, terminator, StandardCharsets.ISO_8859_1);
062        int index = terminator + 1;
063
064        final int compressionFlag = bytes[index++];
065        if (compressionFlag != 0 && compressionFlag != 1) {
066            throw new ImagingException("PNG iTXt chunk has invalid compression flag: " + compressionFlag);
067        }
068
069        final boolean compressed = compressionFlag == 1;
070
071        final int compressionMethod = bytes[index++];
072        if (compressed && compressionMethod != PngConstants.COMPRESSION_DEFLATE_INFLATE) {
073            throw new ImagingException("PNG iTXt chunk has unexpected compression method: " + compressionMethod);
074        }
075
076        terminator = BinaryFunctions.indexOf0(bytes, index, "PNG iTXt chunk language tag is not terminated.");
077        languageTag = new String(bytes, index, terminator - index, StandardCharsets.ISO_8859_1);
078        index = terminator + 1;
079
080        terminator = BinaryFunctions.indexOf0(bytes, index, "PNG iTXt chunk translated keyword is not terminated.");
081        translatedKeyword = new String(bytes, index, terminator - index, StandardCharsets.UTF_8);
082        index = terminator + 1;
083
084        if (compressed) {
085            final int compressedTextLength = bytes.length - index;
086
087            final byte[] compressedText = Allocator.byteArray(compressedTextLength);
088            System.arraycopy(bytes, index, compressedText, 0, compressedTextLength);
089
090            text = new String(IOUtils.toByteArray(new InflaterInputStream(new ByteArrayInputStream(compressedText))), StandardCharsets.UTF_8);
091
092        } else {
093            text = new String(bytes, index, bytes.length - index, StandardCharsets.UTF_8);
094        }
095    }
096
097    @Override
098    public AbstractPngText getContents() {
099        return new AbstractPngText.Itxt(keyword, text, languageTag, translatedKeyword);
100    }
101
102    /**
103     * @return Gets the keyword.
104     */
105    @Override
106    public String getKeyword() {
107        return keyword;
108    }
109
110    /**
111     * @return Gets the text.
112     */
113    @Override
114    public String getText() {
115        return text;
116    }
117
118    public String getTranslatedKeyword() {
119        return translatedKeyword;
120    }
121}