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