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}