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