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.mylzw; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.nio.ByteOrder; 022import java.util.HashMap; 023import java.util.Map; 024 025import org.apache.commons.imaging.ImagingException; 026import org.apache.commons.imaging.common.Allocator; 027 028public class MyLzwCompressor { 029 private static final class ByteArray { 030 private final byte[] bytes; 031 private final int start; 032 private final int length; 033 private final int hash; 034 035 ByteArray(final byte[] bytes, final int start, final int length) { 036 this.bytes = bytes; 037 this.start = start; 038 this.length = length; 039 040 int tempHash = length; 041 042 for (int i = 0; i < length; i++) { 043 final int b = 0xff & bytes[i + start]; 044 tempHash = tempHash + (tempHash << 8) ^ b ^ i; 045 } 046 047 hash = tempHash; 048 } 049 050 @Override 051 public boolean equals(final Object o) { 052 if (o instanceof ByteArray) { 053 final ByteArray other = (ByteArray) o; 054 if (other.hash != hash) { 055 return false; 056 } 057 if (other.length != length) { 058 return false; 059 } 060 061 for (int i = 0; i < length; i++) { 062 if (other.bytes[i + other.start] != bytes[i + start]) { 063 return false; 064 } 065 } 066 067 return true; 068 } 069 return false; 070 } 071 072 @Override 073 public int hashCode() { 074 return hash; 075 } 076 } 077 078 public interface Listener { 079 void clearCode(int code); 080 081 void dataCode(int code); 082 083 void eoiCode(int code); 084 085 void init(int clearCode, int eoiCode); 086 } 087 088 private int codeSize; 089 090 private final int initialCodeSize; 091 private int codes = -1; 092 private final ByteOrder byteOrder; 093 private final boolean earlyLimit; 094 private final int clearCode; 095 private final int eoiCode; 096 097 private final Listener listener; 098 099 private final Map<ByteArray, Integer> map = new HashMap<>(); 100 101 public MyLzwCompressor(final int initialCodeSize, final ByteOrder byteOrder, final boolean earlyLimit) { 102 this(initialCodeSize, byteOrder, earlyLimit, null); 103 } 104 105 public MyLzwCompressor(final int initialCodeSize, final ByteOrder byteOrder, final boolean earlyLimit, final Listener listener) { 106 this.listener = listener; 107 this.byteOrder = byteOrder; 108 this.earlyLimit = earlyLimit; 109 110 this.initialCodeSize = initialCodeSize; 111 112 clearCode = 1 << initialCodeSize; 113 eoiCode = clearCode + 1; 114 115 if (null != listener) { 116 listener.init(clearCode, eoiCode); 117 } 118 119 initializeStringTable(); 120 } 121 122 private boolean addTableEntry(final MyBitOutputStream bos, final byte[] bytes, final int start, final int length) throws IOException { 123 final ByteArray key = arrayToKey(bytes, start, length); 124 return addTableEntry(bos, key); 125 } 126 127 private boolean addTableEntry(final MyBitOutputStream bos, final ByteArray key) throws IOException { 128 boolean cleared = false; 129 130 int limit = 1 << codeSize; 131 if (earlyLimit) { 132 limit--; 133 } 134 135 if (codes == limit) { 136 if (codeSize < 12) { 137 incrementCodeSize(); 138 } else { 139 writeClearCode(bos); 140 clearTable(); 141 cleared = true; 142 } 143 } 144 145 if (!cleared) { 146 map.put(key, codes); 147 codes++; 148 } 149 150 return cleared; 151 } 152 153 private ByteArray arrayToKey(final byte b) { 154 return arrayToKey(new byte[] { b, }, 0, 1); 155 } 156 157 private ByteArray arrayToKey(final byte[] bytes, final int start, final int length) { 158 return new ByteArray(bytes, start, length); 159 } 160 161 private void clearTable() { 162 initializeStringTable(); 163 incrementCodeSize(); 164 } 165 166 private int codeFromString(final byte[] bytes, final int start, final int length) throws ImagingException { 167 final ByteArray key = arrayToKey(bytes, start, length); 168 final Integer code = map.get(key); 169 if (code == null) { 170 throw new ImagingException("CodeFromString"); 171 } 172 return code; 173 } 174 175 public byte[] compress(final byte[] bytes) throws IOException { 176 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Allocator.checkByteArray(bytes.length)); 177 MyBitOutputStream bos = new MyBitOutputStream(baos, byteOrder)) { 178 179 initializeStringTable(); 180 clearTable(); 181 writeClearCode(bos); 182 183 int wStart = 0; 184 int wLength = 0; 185 186 for (int i = 0; i < bytes.length; i++) { 187 if (isInTable(bytes, wStart, wLength + 1)) { 188 wLength++; 189 } else { 190 final int code = codeFromString(bytes, wStart, wLength); 191 writeDataCode(bos, code); 192 addTableEntry(bos, bytes, wStart, wLength + 1); 193 194 wStart = i; 195 wLength = 1; 196 } 197 } 198 199 final int code = codeFromString(bytes, wStart, wLength); 200 writeDataCode(bos, code); 201 writeEoiCode(bos); 202 bos.flushCache(); 203 return baos.toByteArray(); 204 } 205 } 206 207 private void incrementCodeSize() { 208 if (codeSize != 12) { 209 codeSize++; 210 } 211 } 212 213 private void initializeStringTable() { 214 codeSize = initialCodeSize; 215 216 final int initialEntriesCount = (1 << codeSize) + 2; 217 218 map.clear(); 219 for (codes = 0; codes < initialEntriesCount; codes++) { 220 if (codes != clearCode && codes != eoiCode) { 221 final ByteArray key = arrayToKey((byte) codes); 222 223 map.put(key, codes); 224 } 225 } 226 } 227 228 private boolean isInTable(final byte[] bytes, final int start, final int length) { 229 final ByteArray key = arrayToKey(bytes, start, length); 230 231 return map.containsKey(key); 232 } 233 234 private void writeClearCode(final MyBitOutputStream bos) throws IOException { 235 if (null != listener) { 236 listener.dataCode(clearCode); 237 } 238 writeCode(bos, clearCode); 239 } 240 241 private void writeCode(final MyBitOutputStream bos, final int code) throws IOException { 242 bos.writeBits(code, codeSize); 243 } 244 245 private void writeDataCode(final MyBitOutputStream bos, final int code) throws IOException { 246 if (null != listener) { 247 listener.dataCode(code); 248 } 249 writeCode(bos, code); 250 } 251 252 private void writeEoiCode(final MyBitOutputStream bos) throws IOException { 253 if (null != listener) { 254 listener.eoiCode(eoiCode); 255 } 256 writeCode(bos, eoiCode); 257 } 258}