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.itu_t4;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021
022import org.apache.commons.imaging.ImageReadException;
023import org.apache.commons.imaging.ImageWriteException;
024import org.apache.commons.imaging.common.itu_t4.T4_T6_Tables.Entry;
025
026public final class T4AndT6Compression {
027    private static final HuffmanTree<Integer> WHITE_RUN_LENGTHS = new HuffmanTree<>();
028    private static final HuffmanTree<Integer> BLACK_RUN_LENGTHS = new HuffmanTree<>();
029    private static final HuffmanTree<Entry> CONTROL_CODES = new HuffmanTree<>();
030
031    public static final int WHITE = 0;
032    public static final int BLACK = 1;
033
034    static {
035        try {
036            for (final Entry entry : T4_T6_Tables.WHITE_TERMINATING_CODES) {
037                WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value);
038            }
039            for (final Entry entry : T4_T6_Tables.WHITE_MAKE_UP_CODES) {
040                WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value);
041            }
042            for (final Entry entry : T4_T6_Tables.BLACK_TERMINATING_CODES) {
043                BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value);
044            }
045            for (final Entry entry : T4_T6_Tables.BLACK_MAKE_UP_CODES) {
046                BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value);
047            }
048            for (final Entry entry : T4_T6_Tables.ADDITIONAL_MAKE_UP_CODES) {
049                WHITE_RUN_LENGTHS.insert(entry.bitString, entry.value);
050                BLACK_RUN_LENGTHS.insert(entry.bitString, entry.value);
051            }
052            CONTROL_CODES.insert(T4_T6_Tables.EOL.bitString, T4_T6_Tables.EOL);
053            CONTROL_CODES.insert(T4_T6_Tables.EOL13.bitString, T4_T6_Tables.EOL13);
054            CONTROL_CODES.insert(T4_T6_Tables.EOL14.bitString, T4_T6_Tables.EOL14);
055            CONTROL_CODES.insert(T4_T6_Tables.EOL15.bitString, T4_T6_Tables.EOL15);
056            CONTROL_CODES.insert(T4_T6_Tables.EOL16.bitString, T4_T6_Tables.EOL16);
057            CONTROL_CODES.insert(T4_T6_Tables.EOL17.bitString, T4_T6_Tables.EOL17);
058            CONTROL_CODES.insert(T4_T6_Tables.EOL18.bitString, T4_T6_Tables.EOL18);
059            CONTROL_CODES.insert(T4_T6_Tables.EOL19.bitString, T4_T6_Tables.EOL19);
060            CONTROL_CODES.insert(T4_T6_Tables.P.bitString, T4_T6_Tables.P);
061            CONTROL_CODES.insert(T4_T6_Tables.H.bitString, T4_T6_Tables.H);
062            CONTROL_CODES.insert(T4_T6_Tables.V0.bitString, T4_T6_Tables.V0);
063            CONTROL_CODES.insert(T4_T6_Tables.VL1.bitString, T4_T6_Tables.VL1);
064            CONTROL_CODES.insert(T4_T6_Tables.VL2.bitString, T4_T6_Tables.VL2);
065            CONTROL_CODES.insert(T4_T6_Tables.VL3.bitString, T4_T6_Tables.VL3);
066            CONTROL_CODES.insert(T4_T6_Tables.VR1.bitString, T4_T6_Tables.VR1);
067            CONTROL_CODES.insert(T4_T6_Tables.VR2.bitString, T4_T6_Tables.VR2);
068            CONTROL_CODES.insert(T4_T6_Tables.VR3.bitString, T4_T6_Tables.VR3);
069        } catch (final HuffmanTreeException cannotHappen) {
070            throw new Error(cannotHappen);
071        }
072    }
073
074    private T4AndT6Compression() {
075    }
076
077    private static void compress1DLine(final BitInputStreamFlexible inputStream,
078            final BitArrayOutputStream outputStream, final int[] referenceLine, final int width)
079            throws ImageWriteException {
080        int color = WHITE;
081        int runLength = 0;
082
083        for (int x = 0; x < width; x++) {
084            try {
085                final int nextColor = inputStream.readBits(1);
086                if (referenceLine != null) {
087                    referenceLine[x] = nextColor;
088                }
089                if (color == nextColor) {
090                    ++runLength;
091                } else {
092                    writeRunLength(outputStream, runLength, color);
093                    color = nextColor;
094                    runLength = 1;
095                }
096            } catch (final IOException ioException) {
097                throw new ImageWriteException("Error reading image to compress", ioException);
098            }
099        }
100
101        writeRunLength(outputStream, runLength, color);
102    }
103
104    /**
105     * Compressed with the "Modified Huffman" encoding of section 10 in the
106     * TIFF6 specification. No EOLs, no RTC, rows are padded to end on a byte
107     * boundary.
108     *
109     * @param uncompressed
110     * @param width
111     * @param height
112     * @return the compressed data
113     * @throws ImageWriteException
114     */
115    public static byte[] compressModifiedHuffman(final byte[] uncompressed, final int width, final int height)
116            throws ImageWriteException {
117        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed));
118        try (final BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
119            for (int y = 0; y < height; y++) {
120                compress1DLine(inputStream, outputStream, null, width);
121                inputStream.flushCache();
122                outputStream.flush();
123            }
124            return outputStream.toByteArray();
125        }
126    }
127
128    /**
129     * Decompresses the "Modified Huffman" encoding of section 10 in the TIFF6
130     * specification. No EOLs, no RTC, rows are padded to end on a byte
131     * boundary.
132     *
133     * @param compressed
134     * @param width
135     * @param height
136     * @return the decompressed data
137     * @throws ImageReadException
138     */
139    public static byte[] decompressModifiedHuffman(final byte[] compressed,
140            final int width, final int height) throws ImageReadException {
141        try (ByteArrayInputStream baos = new ByteArrayInputStream(compressed);
142                BitInputStreamFlexible inputStream = new BitInputStreamFlexible(baos);
143                BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
144            for (int y = 0; y < height; y++) {
145                int color = WHITE;
146                int rowLength;
147                for (rowLength = 0; rowLength < width;) {
148                    final int runLength = readTotalRunLength(inputStream, color);
149                    for (int i = 0; i < runLength; i++) {
150                        outputStream.writeBit(color);
151                    }
152                    color = 1 - color;
153                    rowLength += runLength;
154                }
155
156                if (rowLength == width) {
157                    inputStream.flushCache();
158                    outputStream.flush();
159                } else if (rowLength > width) {
160                    throw new ImageReadException("Unrecoverable row length error in image row " + y);
161                }
162            }
163            final byte[] ret = outputStream.toByteArray();
164            return ret;
165        } catch (final IOException ioException) {
166            throw new ImageReadException("Error reading image to decompress", ioException);
167        }
168    }
169
170    public static byte[] compressT4_1D(final byte[] uncompressed, final int width,
171            final int height, final boolean hasFill) throws ImageWriteException {
172        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed));
173        try (final BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
174            if (hasFill) {
175                T4_T6_Tables.EOL16.writeBits(outputStream);
176            } else {
177                T4_T6_Tables.EOL.writeBits(outputStream);
178            }
179
180            for (int y = 0; y < height; y++) {
181                compress1DLine(inputStream, outputStream, null, width);
182                if (hasFill) {
183                    int bitsAvailable = outputStream.getBitsAvailableInCurrentByte();
184                    if (bitsAvailable < 4) {
185                        outputStream.flush();
186                        bitsAvailable = 8;
187                    }
188                    for (; bitsAvailable > 4; bitsAvailable--) {
189                        outputStream.writeBit(0);
190                    }
191                }
192                T4_T6_Tables.EOL.writeBits(outputStream);
193                inputStream.flushCache();
194            }
195
196            return outputStream.toByteArray();
197        }
198    }
199
200    /**
201     * Decompresses T.4 1D encoded data. EOL at the beginning and after each
202     * row, can be preceded by fill bits to fit on a byte boundary, no RTC.
203     *
204     * @param compressed
205     * @param width
206     * @param height
207     * @return the decompressed data
208     * @throws ImageReadException
209     */
210    public static byte[] decompressT4_1D(final byte[] compressed, final int width,
211            final int height, final boolean hasFill) throws ImageReadException {
212        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
213        try (BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
214            for (int y = 0; y < height; y++) {
215                int rowLength;
216                try {
217                    final T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream);
218                    if (!isEOL(entry, hasFill)) {
219                        throw new ImageReadException("Expected EOL not found");
220                    }
221                    int color = WHITE;
222                    for (rowLength = 0; rowLength < width;) {
223                        final int runLength = readTotalRunLength(inputStream, color);
224                        for (int i = 0; i < runLength; i++) {
225                            outputStream.writeBit(color);
226                        }
227                        color = 1 - color;
228                        rowLength += runLength;
229                    }
230                } catch (final HuffmanTreeException huffmanException) {
231                    throw new ImageReadException("Decompression error", huffmanException);
232                }
233
234                if (rowLength == width) {
235                    outputStream.flush();
236                } else if (rowLength > width) {
237                    throw new ImageReadException("Unrecoverable row length error in image row " + y);
238                }
239            }
240            final byte[] ret = outputStream.toByteArray();
241            return ret;
242        }
243    }
244
245    private static int compressT(final int a0, final int a1, final int b1, final BitArrayOutputStream outputStream,final  int codingA0Color, final int[] codingLine ){
246          final int a1b1 = a1 - b1;
247          if (-3 <= a1b1 && a1b1 <= 3) {
248              T4_T6_Tables.Entry entry;
249              if (a1b1 == -3) {
250                  entry = T4_T6_Tables.VL3;
251              } else if (a1b1 == -2) {
252                  entry = T4_T6_Tables.VL2;
253              } else if (a1b1 == -1) {
254                  entry = T4_T6_Tables.VL1;
255              } else if (a1b1 == 0) {
256                  entry = T4_T6_Tables.V0;
257              } else if (a1b1 == 1) {
258                  entry = T4_T6_Tables.VR1;
259              } else if (a1b1 == 2) {
260                  entry = T4_T6_Tables.VR2;
261              } else {
262                  entry = T4_T6_Tables.VR3;
263              }
264              entry.writeBits(outputStream);
265              return a1;
266
267          } else {
268              final int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1);
269              final int a0a1 = a1 - a0;
270              final int a1a2 = a2 - a1;
271              T4_T6_Tables.H.writeBits(outputStream);
272              writeRunLength(outputStream, a0a1, codingA0Color);
273              writeRunLength(outputStream, a1a2, 1 - codingA0Color);
274              return a2;
275          }
276    }
277    public static byte[] compressT4_2D(final byte[] uncompressed, final int width,
278            final int height, final boolean hasFill, final int parameterK)
279            throws ImageWriteException {
280        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed));
281        final BitArrayOutputStream outputStream = new BitArrayOutputStream();
282        int[] referenceLine = new int[width];
283        int[] codingLine = new int[width];
284        int kCounter = 0;
285        if (hasFill) {
286            T4_T6_Tables.EOL16.writeBits(outputStream);
287        } else {
288            T4_T6_Tables.EOL.writeBits(outputStream);
289        }
290
291        for (int y = 0; y < height; y++) {
292            if (kCounter > 0) {
293                // 2D
294                outputStream.writeBit(0);
295                for (int i = 0; i < width; i++) {
296                    try {
297                        codingLine[i] = inputStream.readBits(1);
298                    } catch (final IOException ioException) {
299                        throw new ImageWriteException("Error reading image to compress", ioException);
300                    }
301                }
302                int codingA0Color = WHITE;
303                int referenceA0Color = WHITE;
304                int a1 = nextChangingElement(codingLine, codingA0Color, 0);
305                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
306                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
307                for (int a0 = 0; a0 < width;) {
308                    if (b2 < a1) {
309                        T4_T6_Tables.P.writeBits(outputStream);
310                        a0 = b2;
311                    } else {
312                        a0 = compressT(a0, a1, b1, outputStream, codingA0Color, codingLine);
313                        if (a0 == a1) {
314                            codingA0Color = 1 - codingA0Color;
315                        }
316                    }
317                    referenceA0Color = changingElementAt(referenceLine, a0);
318                    a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1);
319                    if (codingA0Color == referenceA0Color) {
320                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
321                    } else {
322                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
323                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
324                    }
325                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
326                }
327                final int[] swap = referenceLine;
328                referenceLine = codingLine;
329                codingLine = swap;
330            } else {
331                // 1D
332                outputStream.writeBit(1);
333                compress1DLine(inputStream, outputStream, referenceLine, width);
334            }
335            if (hasFill) {
336                int bitsAvailable = outputStream.getBitsAvailableInCurrentByte();
337                if (bitsAvailable < 4) {
338                    outputStream.flush();
339                    bitsAvailable = 8;
340                }
341                for (; bitsAvailable > 4; bitsAvailable--) {
342                    outputStream.writeBit(0);
343                }
344            }
345            T4_T6_Tables.EOL.writeBits(outputStream);
346            kCounter++;
347            if (kCounter == parameterK) {
348                kCounter = 0;
349            }
350            inputStream.flushCache();
351        }
352
353        return outputStream.toByteArray();
354    }
355
356    /**
357     * Decompressed T.4 2D encoded data. EOL at the beginning and after each
358     * row, can be preceded by fill bits to fit on a byte boundary, and is
359     * succeeded by a tag bit determining whether the next line is encoded using
360     * 1D or 2D. No RTC.
361     *
362     * @param compressed
363     * @param width
364     * @param height
365     * @return the decompressed data
366     * @throws ImageReadException
367     */
368    public static byte[] decompressT4_2D(final byte[] compressed, final int width,
369            final int height, final boolean hasFill) throws ImageReadException {
370        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
371        final BitArrayOutputStream outputStream = new BitArrayOutputStream();
372        final int[] referenceLine = new int[width];
373        for (int y = 0; y < height; y++) {
374            int rowLength = 0;
375            try {
376                T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream);
377                if (!isEOL(entry, hasFill)) {
378                    throw new ImageReadException("Expected EOL not found");
379                }
380                final int tagBit = inputStream.readBits(1);
381                if (tagBit == 0) {
382                    // 2D
383                    int codingA0Color = WHITE;
384                    int referenceA0Color = WHITE;
385                    int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
386                    int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
387                    for (int a0 = 0; a0 < width;) {
388                        int a1;
389                        int a2;
390                        entry = CONTROL_CODES.decode(inputStream);
391                        if (entry == T4_T6_Tables.P) {
392                            fillRange(outputStream, referenceLine, a0, b2, codingA0Color);
393                            a0 = b2;
394                        } else if (entry == T4_T6_Tables.H) {
395                            final int a0a1 = readTotalRunLength(inputStream, codingA0Color);
396                            a1 = a0 + a0a1;
397                            fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
398                            final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color);
399                            a2 = a1 + a1a2;
400                            fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color);
401                            a0 = a2;
402                        } else {
403                            int a1b1;
404                            if (entry == T4_T6_Tables.V0) {
405                                a1b1 = 0;
406                            } else if (entry == T4_T6_Tables.VL1) {
407                                a1b1 = -1;
408                            } else if (entry == T4_T6_Tables.VL2) {
409                                a1b1 = -2;
410                            } else if (entry == T4_T6_Tables.VL3) {
411                                a1b1 = -3;
412                            } else if (entry == T4_T6_Tables.VR1) {
413                                a1b1 = 1;
414                            } else if (entry == T4_T6_Tables.VR2) {
415                                a1b1 = 2;
416                            } else if (entry == T4_T6_Tables.VR3) {
417                                a1b1 = 3;
418                            } else {
419                                throw new ImageReadException("Invalid/unknown T.4 control code " + entry.bitString);
420                            }
421                            a1 = b1 + a1b1;
422                            fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
423                            a0 = a1;
424                            codingA0Color = 1 - codingA0Color;
425                        }
426                        referenceA0Color = changingElementAt(referenceLine, a0);
427                        if (codingA0Color == referenceA0Color) {
428                            b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
429                        } else {
430                            b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
431                            b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
432                        }
433                        b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
434                        rowLength = a0;
435                    }
436                } else {
437                    // 1D
438                    int color = WHITE;
439                    for (rowLength = 0; rowLength < width;) {
440                        final int runLength = readTotalRunLength(inputStream, color);
441                        for (int i = 0; i < runLength; i++) {
442                            outputStream.writeBit(color);
443                            referenceLine[rowLength + i] = color;
444                        }
445                        color = 1 - color;
446                        rowLength += runLength;
447                    }
448                }
449            } catch (final IOException ioException) {
450                throw new ImageReadException("Decompression error", ioException);
451            } catch (final HuffmanTreeException huffmanException) {
452                throw new ImageReadException("Decompression error", huffmanException);
453            }
454
455            if (rowLength == width) {
456                outputStream.flush();
457            } else if (rowLength > width) {
458                throw new ImageReadException("Unrecoverable row length error in image row " + y);
459            }
460        }
461
462        return outputStream.toByteArray();
463    }
464
465    public static byte[] compressT6(final byte[] uncompressed, final int width, final int height)
466            throws ImageWriteException {
467        try (BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed))) {
468            final BitArrayOutputStream outputStream = new BitArrayOutputStream();
469            int[] referenceLine = new int[width];
470            int[] codingLine = new int[width];
471            for (int y = 0; y < height; y++) {
472                for (int i = 0; i < width; i++) {
473                    try {
474                        codingLine[i] = inputStream.readBits(1);
475                    } catch (final IOException ioException) {
476                        throw new ImageWriteException("Error reading image to compress", ioException);
477                    }
478                }
479                int codingA0Color = WHITE;
480                int referenceA0Color = WHITE;
481                int a1 = nextChangingElement(codingLine, codingA0Color, 0);
482                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
483                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
484                for (int a0 = 0; a0 < width;) {
485                    if (b2 < a1) {
486                        T4_T6_Tables.P.writeBits(outputStream);
487                        a0 = b2;
488                    } else {
489                        a0 = compressT(a0, a1, b1, outputStream, codingA0Color, codingLine);
490                        if (a0 == a1) {
491                            codingA0Color = 1 - codingA0Color;
492                        }
493                    }
494                    referenceA0Color = changingElementAt(referenceLine, a0);
495                    a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1);
496                    if (codingA0Color == referenceA0Color) {
497                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
498                    } else {
499                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
500                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
501                    }
502                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
503                }
504                final int[] swap = referenceLine;
505                referenceLine = codingLine;
506                codingLine = swap;
507                inputStream.flushCache();
508            }
509            // EOFB
510            T4_T6_Tables.EOL.writeBits(outputStream);
511            T4_T6_Tables.EOL.writeBits(outputStream);
512            final byte[] ret = outputStream.toByteArray();
513            return ret;
514        } catch (final IOException ioException) {
515            throw new ImageWriteException("I/O error", ioException);
516        }
517    }
518
519    /**
520     * Decompress T.6 encoded data. No EOLs, except for 2 consecutive ones at
521     * the end (the EOFB, end of fax block). No RTC. No fill bits anywhere. All
522     * data is 2D encoded.
523     *
524     * @param compressed
525     * @param width
526     * @param height
527     * @return the decompressed data
528     * @throws ImageReadException
529     */
530    public static byte[] decompressT6(final byte[] compressed, final int width, final int height)
531            throws ImageReadException {
532        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
533        final BitArrayOutputStream outputStream = new BitArrayOutputStream();
534        final int[] referenceLine = new int[width];
535        for (int y = 0; y < height; y++) {
536            int rowLength = 0;
537            try {
538                int codingA0Color = WHITE;
539                int referenceA0Color = WHITE;
540                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
541                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
542                for (int a0 = 0; a0 < width;) {
543                    int a1;
544                    int a2;
545                    final T4_T6_Tables.Entry  entry = CONTROL_CODES.decode(inputStream);
546                    if (entry == T4_T6_Tables.P) {
547                        fillRange(outputStream, referenceLine, a0, b2, codingA0Color);
548                        a0 = b2;
549                    } else if (entry == T4_T6_Tables.H) {
550                        final int a0a1 = readTotalRunLength(inputStream, codingA0Color);
551                        a1 = a0 + a0a1;
552                        fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
553                        final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color);
554                        a2 = a1 + a1a2;
555                        fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color);
556                        a0 = a2;
557                    } else {
558                        int a1b1;
559                        if (entry == T4_T6_Tables.V0) {
560                            a1b1 = 0;
561                        } else if (entry == T4_T6_Tables.VL1) {
562                            a1b1 = -1;
563                        } else if (entry == T4_T6_Tables.VL2) {
564                            a1b1 = -2;
565                        } else if (entry == T4_T6_Tables.VL3) {
566                            a1b1 = -3;
567                        } else if (entry == T4_T6_Tables.VR1) {
568                            a1b1 = 1;
569                        } else if (entry == T4_T6_Tables.VR2) {
570                            a1b1 = 2;
571                        } else if (entry == T4_T6_Tables.VR3) {
572                            a1b1 = 3;
573                        } else {
574                            throw new ImageReadException("Invalid/unknown T.6 control code " + entry.bitString);
575                        }
576                        a1 = b1 + a1b1;
577                        fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
578                        a0 = a1;
579                        codingA0Color = 1 - codingA0Color;
580                    }
581                    referenceA0Color = changingElementAt(referenceLine, a0);
582                    if (codingA0Color == referenceA0Color) {
583                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
584                    } else {
585                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
586                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
587                    }
588                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
589                    rowLength = a0;
590                }
591            } catch (final HuffmanTreeException huffmanException) {
592                throw new ImageReadException("Decompression error", huffmanException);
593            }
594
595            if (rowLength == width) {
596                outputStream.flush();
597            } else if (rowLength > width) {
598                throw new ImageReadException("Unrecoverable row length error in image row " + y);
599            }
600        }
601
602        return outputStream.toByteArray();
603    }
604
605    private static boolean isEOL(final T4_T6_Tables.Entry entry, final boolean hasFill) {
606        if (entry == T4_T6_Tables.EOL) {
607            return true;
608        }
609        if (hasFill) {
610            return entry == T4_T6_Tables.EOL13 || entry == T4_T6_Tables.EOL14
611                    || entry == T4_T6_Tables.EOL15
612                    || entry == T4_T6_Tables.EOL16
613                    || entry == T4_T6_Tables.EOL17
614                    || entry == T4_T6_Tables.EOL18
615                    || entry == T4_T6_Tables.EOL19;
616        }
617        return false;
618    }
619
620    private static void writeRunLength(final BitArrayOutputStream bitStream,
621            int runLength, final int color) {
622        final T4_T6_Tables.Entry[] makeUpCodes;
623        final T4_T6_Tables.Entry[] terminatingCodes;
624        if (color == WHITE) {
625            makeUpCodes = T4_T6_Tables.WHITE_MAKE_UP_CODES;
626            terminatingCodes = T4_T6_Tables.WHITE_TERMINATING_CODES;
627        } else {
628            makeUpCodes = T4_T6_Tables.BLACK_MAKE_UP_CODES;
629            terminatingCodes = T4_T6_Tables.BLACK_TERMINATING_CODES;
630        }
631        while (runLength >= 1792) {
632            final T4_T6_Tables.Entry entry = lowerBound(
633                    T4_T6_Tables.ADDITIONAL_MAKE_UP_CODES, runLength);
634            entry.writeBits(bitStream);
635            runLength -= entry.value;
636        }
637        while (runLength >= 64) {
638            final T4_T6_Tables.Entry entry = lowerBound(makeUpCodes, runLength);
639            entry.writeBits(bitStream);
640            runLength -= entry.value;
641        }
642        final T4_T6_Tables.Entry terminatingEntry = terminatingCodes[runLength];
643        terminatingEntry.writeBits(bitStream);
644    }
645
646    private static T4_T6_Tables.Entry lowerBound(final T4_T6_Tables.Entry[] entries, final int value) {
647        int first = 0;
648        int last = entries.length - 1;
649        do {
650            final int middle = (first + last) >>> 1;
651            if (entries[middle].value <= value
652                    && ((middle + 1) >= entries.length || value < entries[middle + 1].value)) {
653                return entries[middle];
654            } else if (entries[middle].value > value) {
655                last = middle - 1;
656            } else {
657                first = middle + 1;
658            }
659        } while (first < last);
660
661        return entries[first];
662    }
663
664    private static int readTotalRunLength(final BitInputStreamFlexible bitStream,
665            final int color) throws ImageReadException {
666        try {
667            int totalLength = 0;
668            Integer runLength;
669            do {
670                if (color == WHITE) {
671                    runLength = WHITE_RUN_LENGTHS.decode(bitStream);
672                } else {
673                    runLength = BLACK_RUN_LENGTHS.decode(bitStream);
674                }
675                totalLength += runLength;
676            } while (runLength > 63);
677            return totalLength;
678        } catch (final HuffmanTreeException huffmanException) {
679            throw new ImageReadException("Decompression error", huffmanException);
680        }
681    }
682
683    private static int changingElementAt(final int[] line, final int position) {
684        if (position < 0 || position >= line.length) {
685            return WHITE;
686        }
687        return line[position];
688    }
689
690    private static int nextChangingElement(final int[] line, final int currentColour, final int start) {
691        int position;
692        for (position = start; position < line.length
693                && line[position] == currentColour; position++) {
694            // noop
695        }
696
697        return position < line.length ? position : line.length;
698    }
699
700    private static void fillRange(final BitArrayOutputStream outputStream,
701            final int[] referenceRow, final int a0, final int end, final int color) {
702        for (int i = a0; i < end; i++) {
703            referenceRow[i] = color;
704            outputStream.writeBit(color);
705        }
706    }
707}