View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.mylzw;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.nio.ByteOrder;
24  import java.util.Arrays;
25  
26  import org.apache.commons.imaging.ImagingException;
27  import org.apache.commons.imaging.common.Allocator;
28  
29  public final class MyLzwDecompressor {
30  
31      public interface Listener {
32  
33          void code(int code);
34  
35          void init(int clearCode, int eoiCode);
36      }
37  
38      private static final int MAX_TABLE_SIZE = 1 << 12;
39      private final byte[][] table;
40      private int codeSize;
41      private final int initialCodeSize;
42      private int codes = -1;
43      private final ByteOrder byteOrder;
44      private final Listener listener;
45      private final int clearCode;
46      private final int eoiCode;
47      private int written;
48      private final boolean tiffLZWMode;
49  
50      public MyLzwDecompressor(final int initialCodeSize, final ByteOrder byteOrder, final boolean tiffLZWMode) throws ImagingException {
51          this(initialCodeSize, byteOrder, tiffLZWMode, null);
52      }
53  
54      public MyLzwDecompressor(final int initialCodeSize, final ByteOrder byteOrder, final boolean tiffLZWMode, final Listener listener) throws ImagingException {
55          this.listener = listener;
56          this.byteOrder = byteOrder;
57          this.tiffLZWMode = tiffLZWMode;
58          this.initialCodeSize = initialCodeSize;
59  
60          table = new byte[MAX_TABLE_SIZE][];
61          clearCode = 1 << initialCodeSize;
62          eoiCode = clearCode + 1;
63  
64          if (null != listener) {
65              listener.init(clearCode, eoiCode);
66          }
67  
68          initializeTable();
69      }
70  
71      private void addStringToTable(final byte[] bytes) {
72          if (codes < 1 << codeSize) {
73              table[codes] = bytes;
74              codes++;
75          }
76          // If the table already full, then we simply ignore these bytes
77          // per the https://www.w3.org/Graphics/GIF/spec-gif89a.txt 'spec'.
78  
79          checkCodeSize();
80      }
81  
82      private byte[] appendBytes(final byte[] bytes, final byte b) {
83          final byte[] result = Arrays.copyOf(bytes, bytes.length + 1);
84          result[result.length - 1] = b;
85          return result;
86      }
87  
88      private void checkCodeSize() {
89          int limit = 1 << codeSize;
90          if (tiffLZWMode) {
91              limit--;
92          }
93  
94          if (codes == limit) {
95              incrementCodeSize();
96          }
97      }
98  
99      private void clearTable() {
100         codes = (1 << initialCodeSize) + 2;
101         codeSize = initialCodeSize;
102         incrementCodeSize();
103     }
104 
105     public byte[] decompress(final InputStream is, final int expectedLength) throws IOException {
106         int code;
107         int oldCode = -1;
108         try (MyBitInputStream mbis = new MyBitInputStream(is, byteOrder, tiffLZWMode);
109                 ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArray(expectedLength))) {
110 
111             clearTable();
112 
113             while ((code = getNextCode(mbis)) != eoiCode) {
114                 if (code == clearCode) {
115                     clearTable();
116 
117                     if (written >= expectedLength) {
118                         break;
119                     }
120                     code = getNextCode(mbis);
121 
122                     if (code == eoiCode) {
123                         break;
124                     }
125                     writeToResult(baos, stringFromCode(code));
126                 } else if (isInTable(code)) {
127                     writeToResult(baos, stringFromCode(code));
128 
129                     addStringToTable(appendBytes(stringFromCode(oldCode), firstChar(stringFromCode(code))));
130                 } else {
131                     final byte[] outString = appendBytes(stringFromCode(oldCode), firstChar(stringFromCode(oldCode)));
132                     writeToResult(baos, outString);
133                     addStringToTable(outString);
134                 }
135                 oldCode = code;
136 
137                 if (written >= expectedLength) {
138                     break;
139                 }
140             }
141 
142             return baos.toByteArray();
143         }
144     }
145 
146     private byte firstChar(final byte[] bytes) {
147         return bytes[0];
148     }
149 
150     private int getNextCode(final MyBitInputStream is) throws IOException {
151         final int code = is.readBits(codeSize);
152 
153         if (null != listener) {
154             listener.code(code);
155         }
156         return code;
157     }
158 
159     private void incrementCodeSize() {
160         if (codeSize != 12) {
161             codeSize++;
162         }
163     }
164 
165     private void initializeTable() throws ImagingException {
166         codeSize = initialCodeSize;
167 
168         final int initialEntriesCount = 1 << codeSize + 2;
169 
170         if (initialEntriesCount > table.length) {
171             throw new ImagingException(String.format("Invalid Lzw table length [%d]; entries count is [%d]", table.length, initialEntriesCount));
172         }
173 
174         for (int i = 0; i < initialEntriesCount; i++) {
175             table[i] = new byte[] { (byte) i, };
176         }
177     }
178 
179     private boolean isInTable(final int code) {
180         return code < codes;
181     }
182 
183     private byte[] stringFromCode(final int code) throws ImagingException {
184         if (code >= codes || code < 0) {
185             throw new ImagingException("Bad Code: " + code + " codes: " + codes + " code_size: " + codeSize + ", table: " + table.length);
186         }
187         return table[code];
188     }
189 
190     private void writeToResult(final OutputStream os, final byte[] bytes) throws IOException {
191         os.write(bytes);
192         written += bytes.length;
193     }
194 }