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 uncompressed byte data
110     * @param width image width
111     * @param height image height
112     * @return the compressed data
113     * @throws ImageWriteException if it fails to write the compressed data
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 (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 compressed byte data
134     * @param width image width
135     * @param height image height
136     * @return the compressed data
137     * @throws ImageReadException if it fails to read the compressed data
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 (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 compressed byte data
205     * @param width image width
206     * @param height image height
207     * @param hasFill used to check the end of line
208     * @return the decompressed data
209     * @throws ImageReadException if it fails to read the compressed data
210     */
211    public static byte[] decompressT4_1D(final byte[] compressed, final int width,
212            final int height, final boolean hasFill) throws ImageReadException {
213        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
214        try (BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
215            for (int y = 0; y < height; y++) {
216                int rowLength;
217                try {
218                    final T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream);
219                    if (!isEOL(entry, hasFill)) {
220                        throw new ImageReadException("Expected EOL not found");
221                    }
222                    int color = WHITE;
223                    for (rowLength = 0; rowLength < width;) {
224                        final int runLength = readTotalRunLength(inputStream, color);
225                        for (int i = 0; i < runLength; i++) {
226                            outputStream.writeBit(color);
227                        }
228                        color = 1 - color;
229                        rowLength += runLength;
230                    }
231                } catch (final HuffmanTreeException huffmanException) {
232                    throw new ImageReadException("Decompression error", huffmanException);
233                }
234
235                if (rowLength == width) {
236                    outputStream.flush();
237                } else if (rowLength > width) {
238                    throw new ImageReadException("Unrecoverable row length error in image row " + y);
239                }
240            }
241            final byte[] ret = outputStream.toByteArray();
242            return ret;
243        }
244    }
245
246    private static int compressT(final int a0, final int a1, final int b1, final BitArrayOutputStream outputStream,final  int codingA0Color, final int[] codingLine ){
247          final int a1b1 = a1 - b1;
248          if (-3 <= a1b1 && a1b1 <= 3) {
249              T4_T6_Tables.Entry entry;
250              if (a1b1 == -3) {
251                  entry = T4_T6_Tables.VL3;
252              } else if (a1b1 == -2) {
253                  entry = T4_T6_Tables.VL2;
254              } else if (a1b1 == -1) {
255                  entry = T4_T6_Tables.VL1;
256              } else if (a1b1 == 0) {
257                  entry = T4_T6_Tables.V0;
258              } else if (a1b1 == 1) {
259                  entry = T4_T6_Tables.VR1;
260              } else if (a1b1 == 2) {
261                  entry = T4_T6_Tables.VR2;
262              } else {
263                  entry = T4_T6_Tables.VR3;
264              }
265              entry.writeBits(outputStream);
266              return a1;
267
268          } else {
269              final int a2 = nextChangingElement(codingLine, 1 - codingA0Color, a1 + 1);
270              final int a0a1 = a1 - a0;
271              final int a1a2 = a2 - a1;
272              T4_T6_Tables.H.writeBits(outputStream);
273              writeRunLength(outputStream, a0a1, codingA0Color);
274              writeRunLength(outputStream, a1a2, 1 - codingA0Color);
275              return a2;
276          }
277    }
278    public static byte[] compressT4_2D(final byte[] uncompressed, final int width,
279            final int height, final boolean hasFill, final int parameterK)
280            throws ImageWriteException {
281        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(uncompressed));
282        final BitArrayOutputStream outputStream = new BitArrayOutputStream();
283        int[] referenceLine = new int[width];
284        int[] codingLine = new int[width];
285        int kCounter = 0;
286        if (hasFill) {
287            T4_T6_Tables.EOL16.writeBits(outputStream);
288        } else {
289            T4_T6_Tables.EOL.writeBits(outputStream);
290        }
291
292        for (int y = 0; y < height; y++) {
293            if (kCounter > 0) {
294                // 2D
295                outputStream.writeBit(0);
296                for (int i = 0; i < width; i++) {
297                    try {
298                        codingLine[i] = inputStream.readBits(1);
299                    } catch (final IOException ioException) {
300                        throw new ImageWriteException("Error reading image to compress", ioException);
301                    }
302                }
303                int codingA0Color = WHITE;
304                int referenceA0Color = WHITE;
305                int a1 = nextChangingElement(codingLine, codingA0Color, 0);
306                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
307                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
308                for (int a0 = 0; a0 < width;) {
309                    if (b2 < a1) {
310                        T4_T6_Tables.P.writeBits(outputStream);
311                        a0 = b2;
312                    } else {
313                        a0 = compressT(a0, a1, b1, outputStream, codingA0Color, codingLine);
314                        if (a0 == a1) {
315                            codingA0Color = 1 - codingA0Color;
316                        }
317                    }
318                    referenceA0Color = changingElementAt(referenceLine, a0);
319                    a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1);
320                    if (codingA0Color == referenceA0Color) {
321                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
322                    } else {
323                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
324                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
325                    }
326                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
327                }
328                final int[] swap = referenceLine;
329                referenceLine = codingLine;
330                codingLine = swap;
331            } else {
332                // 1D
333                outputStream.writeBit(1);
334                compress1DLine(inputStream, outputStream, referenceLine, width);
335            }
336            if (hasFill) {
337                int bitsAvailable = outputStream.getBitsAvailableInCurrentByte();
338                if (bitsAvailable < 4) {
339                    outputStream.flush();
340                    bitsAvailable = 8;
341                }
342                for (; bitsAvailable > 4; bitsAvailable--) {
343                    outputStream.writeBit(0);
344                }
345            }
346            T4_T6_Tables.EOL.writeBits(outputStream);
347            kCounter++;
348            if (kCounter == parameterK) {
349                kCounter = 0;
350            }
351            inputStream.flushCache();
352        }
353
354        return outputStream.toByteArray();
355    }
356
357    /**
358     * Decompressed T.4 2D encoded data. EOL at the beginning and after each
359     * row, can be preceded by fill bits to fit on a byte boundary, and is
360     * succeeded by a tag bit determining whether the next line is encoded using
361     * 1D or 2D. No RTC.
362     *
363     * @param compressed compressed byte data
364     * @param width image width
365     * @param height image height
366     * @param hasFill used to check the end of line
367     * @return the decompressed data
368     * @throws ImageReadException if it fails to read the compressed data
369     */
370    public static byte[] decompressT4_2D(final byte[] compressed, final int width,
371            final int height, final boolean hasFill) throws ImageReadException {
372        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
373        try (BitArrayOutputStream outputStream = new BitArrayOutputStream()) {
374            final int[] referenceLine = new int[width];
375            for (int y = 0; y < height; y++) {
376                int rowLength = 0;
377                try {
378                    T4_T6_Tables.Entry entry = CONTROL_CODES.decode(inputStream);
379                    if (!isEOL(entry, hasFill)) {
380                        throw new ImageReadException("Expected EOL not found");
381                    }
382                    final int tagBit = inputStream.readBits(1);
383                    if (tagBit == 0) {
384                        // 2D
385                        int codingA0Color = WHITE;
386                        int referenceA0Color = WHITE;
387                        int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
388                        int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
389                        for (int a0 = 0; a0 < width;) {
390                            int a1;
391                            int a2;
392                            entry = CONTROL_CODES.decode(inputStream);
393                            if (entry == T4_T6_Tables.P) {
394                                fillRange(outputStream, referenceLine, a0, b2, codingA0Color);
395                                a0 = b2;
396                            } else if (entry == T4_T6_Tables.H) {
397                                final int a0a1 = readTotalRunLength(inputStream, codingA0Color);
398                                a1 = a0 + a0a1;
399                                fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
400                                final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color);
401                                a2 = a1 + a1a2;
402                                fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color);
403                                a0 = a2;
404                            } else {
405                                int a1b1;
406                                if (entry == T4_T6_Tables.V0) {
407                                    a1b1 = 0;
408                                } else if (entry == T4_T6_Tables.VL1) {
409                                    a1b1 = -1;
410                                } else if (entry == T4_T6_Tables.VL2) {
411                                    a1b1 = -2;
412                                } else if (entry == T4_T6_Tables.VL3) {
413                                    a1b1 = -3;
414                                } else if (entry == T4_T6_Tables.VR1) {
415                                    a1b1 = 1;
416                                } else if (entry == T4_T6_Tables.VR2) {
417                                    a1b1 = 2;
418                                } else if (entry == T4_T6_Tables.VR3) {
419                                    a1b1 = 3;
420                                } else {
421                                    throw new ImageReadException("Invalid/unknown T.4 control code " + entry.bitString);
422                                }
423                                a1 = b1 + a1b1;
424                                fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
425                                a0 = a1;
426                                codingA0Color = 1 - codingA0Color;
427                            }
428                            referenceA0Color = changingElementAt(referenceLine, a0);
429                            if (codingA0Color == referenceA0Color) {
430                                b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
431                            } else {
432                                b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
433                                b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
434                            }
435                            b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
436                            rowLength = a0;
437                        }
438                    } else {
439                        // 1D
440                        int color = WHITE;
441                        for (rowLength = 0; rowLength < width;) {
442                            final int runLength = readTotalRunLength(inputStream, color);
443                            for (int i = 0; i < runLength; i++) {
444                                outputStream.writeBit(color);
445                                referenceLine[rowLength + i] = color;
446                            }
447                            color = 1 - color;
448                            rowLength += runLength;
449                        }
450                    }
451                } catch (final IOException ioException) {
452                    throw new ImageReadException("Decompression error", ioException);
453                } catch (final HuffmanTreeException huffmanException) {
454                    throw new ImageReadException("Decompression error", huffmanException);
455                }
456
457                if (rowLength == width) {
458                    outputStream.flush();
459                } else if (rowLength > width) {
460                    throw new ImageReadException("Unrecoverable row length error in image row " + y);
461                }
462            }
463
464            return outputStream.toByteArray();
465        }
466    }
467
468    public static byte[] compressT6(final byte[] uncompressed, final int width, final int height)
469            throws ImageWriteException {
470        try (ByteArrayInputStream bais = new ByteArrayInputStream(uncompressed);
471                BitInputStreamFlexible inputStream = new BitInputStreamFlexible(bais)) {
472            final BitArrayOutputStream outputStream = new BitArrayOutputStream();
473            int[] referenceLine = new int[width];
474            int[] codingLine = new int[width];
475            for (int y = 0; y < height; y++) {
476                for (int i = 0; i < width; i++) {
477                    try {
478                        codingLine[i] = inputStream.readBits(1);
479                    } catch (final IOException ioException) {
480                        throw new ImageWriteException("Error reading image to compress", ioException);
481                    }
482                }
483                int codingA0Color = WHITE;
484                int referenceA0Color = WHITE;
485                int a1 = nextChangingElement(codingLine, codingA0Color, 0);
486                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
487                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
488                for (int a0 = 0; a0 < width;) {
489                    if (b2 < a1) {
490                        T4_T6_Tables.P.writeBits(outputStream);
491                        a0 = b2;
492                    } else {
493                        a0 = compressT(a0, a1, b1, outputStream, codingA0Color, codingLine);
494                        if (a0 == a1) {
495                            codingA0Color = 1 - codingA0Color;
496                        }
497                    }
498                    referenceA0Color = changingElementAt(referenceLine, a0);
499                    a1 = nextChangingElement(codingLine, codingA0Color, a0 + 1);
500                    if (codingA0Color == referenceA0Color) {
501                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
502                    } else {
503                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
504                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
505                    }
506                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
507                }
508                final int[] swap = referenceLine;
509                referenceLine = codingLine;
510                codingLine = swap;
511                inputStream.flushCache();
512            }
513            // EOFB
514            T4_T6_Tables.EOL.writeBits(outputStream);
515            T4_T6_Tables.EOL.writeBits(outputStream);
516            final byte[] ret = outputStream.toByteArray();
517            return ret;
518        } catch (final IOException ioException) {
519            throw new ImageWriteException("I/O error", ioException);
520        }
521    }
522
523    /**
524     * Decompress T.6 encoded data. No EOLs, except for 2 consecutive ones at
525     * the end (the EOFB, end of fax block). No RTC. No fill bits anywhere. All
526     * data is 2D encoded.
527     *
528     * @param compressed compressed byte data
529     * @param width image width
530     * @param height image height
531     * @return the decompressed data
532     * @throws ImageReadException if it fails to read the compressed data
533     */
534    public static byte[] decompressT6(final byte[] compressed, final int width, final int height)
535            throws ImageReadException {
536        final BitInputStreamFlexible inputStream = new BitInputStreamFlexible(new ByteArrayInputStream(compressed));
537        final BitArrayOutputStream outputStream = new BitArrayOutputStream();
538        final int[] referenceLine = new int[width];
539        for (int y = 0; y < height; y++) {
540            int rowLength = 0;
541            try {
542                int codingA0Color = WHITE;
543                int referenceA0Color = WHITE;
544                int b1 = nextChangingElement(referenceLine, referenceA0Color, 0);
545                int b2 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
546                for (int a0 = 0; a0 < width;) {
547                    int a1;
548                    int a2;
549                    final T4_T6_Tables.Entry  entry = CONTROL_CODES.decode(inputStream);
550                    if (entry == T4_T6_Tables.P) {
551                        fillRange(outputStream, referenceLine, a0, b2, codingA0Color);
552                        a0 = b2;
553                    } else if (entry == T4_T6_Tables.H) {
554                        final int a0a1 = readTotalRunLength(inputStream, codingA0Color);
555                        a1 = a0 + a0a1;
556                        fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
557                        final int a1a2 = readTotalRunLength(inputStream, 1 - codingA0Color);
558                        a2 = a1 + a1a2;
559                        fillRange(outputStream, referenceLine, a1, a2, 1 - codingA0Color);
560                        a0 = a2;
561                    } else {
562                        int a1b1;
563                        if (entry == T4_T6_Tables.V0) {
564                            a1b1 = 0;
565                        } else if (entry == T4_T6_Tables.VL1) {
566                            a1b1 = -1;
567                        } else if (entry == T4_T6_Tables.VL2) {
568                            a1b1 = -2;
569                        } else if (entry == T4_T6_Tables.VL3) {
570                            a1b1 = -3;
571                        } else if (entry == T4_T6_Tables.VR1) {
572                            a1b1 = 1;
573                        } else if (entry == T4_T6_Tables.VR2) {
574                            a1b1 = 2;
575                        } else if (entry == T4_T6_Tables.VR3) {
576                            a1b1 = 3;
577                        } else {
578                            throw new ImageReadException("Invalid/unknown T.6 control code " + entry.bitString);
579                        }
580                        a1 = b1 + a1b1;
581                        fillRange(outputStream, referenceLine, a0, a1, codingA0Color);
582                        a0 = a1;
583                        codingA0Color = 1 - codingA0Color;
584                    }
585                    referenceA0Color = changingElementAt(referenceLine, a0);
586                    if (codingA0Color == referenceA0Color) {
587                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
588                    } else {
589                        b1 = nextChangingElement(referenceLine, referenceA0Color, a0 + 1);
590                        b1 = nextChangingElement(referenceLine, 1 - referenceA0Color, b1 + 1);
591                    }
592                    b2 = nextChangingElement(referenceLine, 1 - codingA0Color, b1 + 1);
593                    rowLength = a0;
594                }
595            } catch (final HuffmanTreeException huffmanException) {
596                throw new ImageReadException("Decompression error", huffmanException);
597            }
598
599            if (rowLength == width) {
600                outputStream.flush();
601            } else if (rowLength > width) {
602                throw new ImageReadException("Unrecoverable row length error in image row " + y);
603            }
604        }
605
606        return outputStream.toByteArray();
607    }
608
609    private static boolean isEOL(final T4_T6_Tables.Entry entry, final boolean hasFill) {
610        if (entry == T4_T6_Tables.EOL) {
611            return true;
612        }
613        if (hasFill) {
614            return entry == T4_T6_Tables.EOL13 || entry == T4_T6_Tables.EOL14
615                    || entry == T4_T6_Tables.EOL15
616                    || entry == T4_T6_Tables.EOL16
617                    || entry == T4_T6_Tables.EOL17
618                    || entry == T4_T6_Tables.EOL18
619                    || entry == T4_T6_Tables.EOL19;
620        }
621        return false;
622    }
623
624    private static void writeRunLength(final BitArrayOutputStream bitStream,
625            int runLength, final int color) {
626        final T4_T6_Tables.Entry[] makeUpCodes;
627        final T4_T6_Tables.Entry[] terminatingCodes;
628        if (color == WHITE) {
629            makeUpCodes = T4_T6_Tables.WHITE_MAKE_UP_CODES;
630            terminatingCodes = T4_T6_Tables.WHITE_TERMINATING_CODES;
631        } else {
632            makeUpCodes = T4_T6_Tables.BLACK_MAKE_UP_CODES;
633            terminatingCodes = T4_T6_Tables.BLACK_TERMINATING_CODES;
634        }
635        while (runLength >= 1792) {
636            final T4_T6_Tables.Entry entry = lowerBound(
637                    T4_T6_Tables.ADDITIONAL_MAKE_UP_CODES, runLength);
638            entry.writeBits(bitStream);
639            runLength -= entry.value;
640        }
641        while (runLength >= 64) {
642            final T4_T6_Tables.Entry entry = lowerBound(makeUpCodes, runLength);
643            entry.writeBits(bitStream);
644            runLength -= entry.value;
645        }
646        final T4_T6_Tables.Entry terminatingEntry = terminatingCodes[runLength];
647        terminatingEntry.writeBits(bitStream);
648    }
649
650    private static T4_T6_Tables.Entry lowerBound(final T4_T6_Tables.Entry[] entries, final int value) {
651        int first = 0;
652        int last = entries.length - 1;
653        do {
654            final int middle = (first + last) >>> 1;
655            if (entries[middle].value <= value
656                    && ((middle + 1) >= entries.length || value < entries[middle + 1].value)) {
657                return entries[middle];
658            } else if (entries[middle].value > value) {
659                last = middle - 1;
660            } else {
661                first = middle + 1;
662            }
663        } while (first < last);
664
665        return entries[first];
666    }
667
668    private static int readTotalRunLength(final BitInputStreamFlexible bitStream,
669            final int color) throws ImageReadException {
670        try {
671            int totalLength = 0;
672            Integer runLength;
673            do {
674                if (color == WHITE) {
675                    runLength = WHITE_RUN_LENGTHS.decode(bitStream);
676                } else {
677                    runLength = BLACK_RUN_LENGTHS.decode(bitStream);
678                }
679                totalLength += runLength;
680            } while (runLength > 63);
681            return totalLength;
682        } catch (final HuffmanTreeException huffmanException) {
683            throw new ImageReadException("Decompression error", huffmanException);
684        }
685    }
686
687    private static int changingElementAt(final int[] line, final int position) {
688        if (position < 0 || position >= line.length) {
689            return WHITE;
690        }
691        return line[position];
692    }
693
694    private static int nextChangingElement(final int[] line, final int currentColour, final int start) {
695        int position;
696        for (position = start; position < line.length
697                && line[position] == currentColour; position++) {
698            // noop
699        }
700
701        return Math.min(position, line.length);
702    }
703
704    private static void fillRange(final BitArrayOutputStream outputStream,
705            final int[] referenceRow, final int a0, final int end, final int color) {
706        for (int i = a0; i < end; i++) {
707            referenceRow[i] = color;
708            outputStream.writeBit(color);
709        }
710    }
711}