1 /* 2 * Copyright 2010 ZXing authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.zxing.oned; 18 19 import com.google.zxing.BarcodeFormat; 20 import com.google.zxing.EncodeHintType; 21 import com.google.zxing.common.BitMatrix; 22 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.Map; 27 28 /** 29 * This object renders a CODE128 code as a {@link BitMatrix}. 30 * 31 * @author erik.barbara@gmail.com (Erik Barbara) 32 */ 33 public final class Code128Writer extends OneDimensionalCodeWriter { 34 35 private static final int CODE_START_A = 103; 36 private static final int CODE_START_B = 104; 37 private static final int CODE_START_C = 105; 38 private static final int CODE_CODE_A = 101; 39 private static final int CODE_CODE_B = 100; 40 private static final int CODE_CODE_C = 99; 41 private static final int CODE_STOP = 106; 42 43 // Dummy characters used to specify control characters in input 44 private static final char ESCAPE_FNC_1 = '\u00f1'; 45 private static final char ESCAPE_FNC_2 = '\u00f2'; 46 private static final char ESCAPE_FNC_3 = '\u00f3'; 47 private static final char ESCAPE_FNC_4 = '\u00f4'; 48 49 private static final int CODE_FNC_1 = 102; // Code A, Code B, Code C 50 private static final int CODE_FNC_2 = 97; // Code A, Code B 51 private static final int CODE_FNC_3 = 96; // Code A, Code B 52 private static final int CODE_FNC_4_A = 101; // Code A 53 private static final int CODE_FNC_4_B = 100; // Code B 54 55 // Results of minimal lookahead for code C 56 private enum CType { 57 UNCODABLE, 58 ONE_DIGIT, 59 TWO_DIGITS, 60 FNC_1 61 } 62 63 @Override getSupportedWriteFormats()64 protected Collection<BarcodeFormat> getSupportedWriteFormats() { 65 return Collections.singleton(BarcodeFormat.CODE_128); 66 } 67 68 @Override encode(String contents)69 public boolean[] encode(String contents) { 70 return encode(contents, null); 71 } 72 73 @Override encode(String contents, Map<EncodeHintType,?> hints)74 public boolean[] encode(String contents, Map<EncodeHintType,?> hints) { 75 76 int forcedCodeSet = check(contents, hints); 77 78 boolean hasCompactionHint = hints != null && hints.containsKey(EncodeHintType.CODE128_COMPACT) && 79 Boolean.parseBoolean(hints.get(EncodeHintType.CODE128_COMPACT).toString()); 80 81 return hasCompactionHint ? new MinimalEncoder().encode(contents) : encodeFast(contents, forcedCodeSet); 82 } 83 check(String contents, Map<EncodeHintType,?> hints)84 private static int check(String contents, Map<EncodeHintType,?> hints) { 85 int length = contents.length(); 86 // Check length 87 if (length < 1 || length > 80) { 88 throw new IllegalArgumentException( 89 "Contents length should be between 1 and 80 characters, but got " + length); 90 } 91 92 // Check for forced code set hint. 93 int forcedCodeSet = -1; 94 if (hints != null && hints.containsKey(EncodeHintType.FORCE_CODE_SET)) { 95 String codeSetHint = hints.get(EncodeHintType.FORCE_CODE_SET).toString(); 96 switch (codeSetHint) { 97 case "A": 98 forcedCodeSet = CODE_CODE_A; 99 break; 100 case "B": 101 forcedCodeSet = CODE_CODE_B; 102 break; 103 case "C": 104 forcedCodeSet = CODE_CODE_C; 105 break; 106 default: 107 throw new IllegalArgumentException("Unsupported code set hint: " + codeSetHint); 108 } 109 } 110 111 // Check content 112 for (int i = 0; i < length; i++) { 113 char c = contents.charAt(i); 114 // check for non ascii characters that are not special GS1 characters 115 switch (c) { 116 // special function characters 117 case ESCAPE_FNC_1: 118 case ESCAPE_FNC_2: 119 case ESCAPE_FNC_3: 120 case ESCAPE_FNC_4: 121 break; 122 // non ascii characters 123 default: 124 if (c > 127) { 125 // no full Latin-1 character set available at the moment 126 // shift and manual code change are not supported 127 throw new IllegalArgumentException("Bad character in input: ASCII value=" + (int) c); 128 } 129 } 130 // check characters for compatibility with forced code set 131 switch (forcedCodeSet) { 132 case CODE_CODE_A: 133 // allows no ascii above 95 (no lower caps, no special symbols) 134 if (c > 95 && c <= 127) { 135 throw new IllegalArgumentException("Bad character in input for forced code set A: ASCII value=" + (int) c); 136 } 137 break; 138 case CODE_CODE_B: 139 // allows no ascii below 32 (terminal symbols) 140 if (c < 32) { 141 throw new IllegalArgumentException("Bad character in input for forced code set B: ASCII value=" + (int) c); 142 } 143 break; 144 case CODE_CODE_C: 145 // allows only numbers and no FNC 2/3/4 146 if (c < 48 || (c > 57 && c <= 127) || c == ESCAPE_FNC_2 || c == ESCAPE_FNC_3 || c == ESCAPE_FNC_4) { 147 throw new IllegalArgumentException("Bad character in input for forced code set C: ASCII value=" + (int) c); 148 } 149 break; 150 } 151 } 152 return forcedCodeSet; 153 } 154 encodeFast(String contents, int forcedCodeSet)155 private static boolean[] encodeFast(String contents, int forcedCodeSet) { 156 int length = contents.length(); 157 158 Collection<int[]> patterns = new ArrayList<>(); // temporary storage for patterns 159 int checkSum = 0; 160 int checkWeight = 1; 161 int codeSet = 0; // selected code (CODE_CODE_B or CODE_CODE_C) 162 int position = 0; // position in contents 163 164 while (position < length) { 165 //Select code to use 166 int newCodeSet; 167 if (forcedCodeSet == -1) { 168 newCodeSet = chooseCode(contents, position, codeSet); 169 } else { 170 newCodeSet = forcedCodeSet; 171 } 172 173 //Get the pattern index 174 int patternIndex; 175 if (newCodeSet == codeSet) { 176 // Encode the current character 177 // First handle escapes 178 switch (contents.charAt(position)) { 179 case ESCAPE_FNC_1: 180 patternIndex = CODE_FNC_1; 181 break; 182 case ESCAPE_FNC_2: 183 patternIndex = CODE_FNC_2; 184 break; 185 case ESCAPE_FNC_3: 186 patternIndex = CODE_FNC_3; 187 break; 188 case ESCAPE_FNC_4: 189 if (codeSet == CODE_CODE_A) { 190 patternIndex = CODE_FNC_4_A; 191 } else { 192 patternIndex = CODE_FNC_4_B; 193 } 194 break; 195 default: 196 // Then handle normal characters otherwise 197 switch (codeSet) { 198 case CODE_CODE_A: 199 patternIndex = contents.charAt(position) - ' '; 200 if (patternIndex < 0) { 201 // everything below a space character comes behind the underscore in the code patterns table 202 patternIndex += '`'; 203 } 204 break; 205 case CODE_CODE_B: 206 patternIndex = contents.charAt(position) - ' '; 207 break; 208 default: 209 // CODE_CODE_C 210 if (position + 1 == length) { 211 // this is the last character, but the encoding is C, which always encodes two characers 212 throw new IllegalArgumentException("Bad number of characters for digit only encoding."); 213 } 214 patternIndex = Integer.parseInt(contents.substring(position, position + 2)); 215 position++; // Also incremented below 216 break; 217 } 218 } 219 position++; 220 } else { 221 // Should we change the current code? 222 // Do we have a code set? 223 if (codeSet == 0) { 224 // No, we don't have a code set 225 switch (newCodeSet) { 226 case CODE_CODE_A: 227 patternIndex = CODE_START_A; 228 break; 229 case CODE_CODE_B: 230 patternIndex = CODE_START_B; 231 break; 232 default: 233 patternIndex = CODE_START_C; 234 break; 235 } 236 } else { 237 // Yes, we have a code set 238 patternIndex = newCodeSet; 239 } 240 codeSet = newCodeSet; 241 } 242 243 // Get the pattern 244 patterns.add(Code128Reader.CODE_PATTERNS[patternIndex]); 245 246 // Compute checksum 247 checkSum += patternIndex * checkWeight; 248 if (position != 0) { 249 checkWeight++; 250 } 251 } 252 return produceResult(patterns, checkSum); 253 } 254 produceResult(Collection<int[]> patterns, int checkSum)255 static boolean[] produceResult(Collection<int[]> patterns, int checkSum) { 256 // Compute and append checksum 257 checkSum %= 103; 258 patterns.add(Code128Reader.CODE_PATTERNS[checkSum]); 259 260 // Append stop code 261 patterns.add(Code128Reader.CODE_PATTERNS[CODE_STOP]); 262 263 // Compute code width 264 int codeWidth = 0; 265 for (int[] pattern : patterns) { 266 for (int width : pattern) { 267 codeWidth += width; 268 } 269 } 270 271 // Compute result 272 boolean[] result = new boolean[codeWidth]; 273 int pos = 0; 274 for (int[] pattern : patterns) { 275 pos += appendPattern(result, pos, pattern, true); 276 } 277 278 return result; 279 } 280 findCType(CharSequence value, int start)281 private static CType findCType(CharSequence value, int start) { 282 int last = value.length(); 283 if (start >= last) { 284 return CType.UNCODABLE; 285 } 286 char c = value.charAt(start); 287 if (c == ESCAPE_FNC_1) { 288 return CType.FNC_1; 289 } 290 if (c < '0' || c > '9') { 291 return CType.UNCODABLE; 292 } 293 if (start + 1 >= last) { 294 return CType.ONE_DIGIT; 295 } 296 c = value.charAt(start + 1); 297 if (c < '0' || c > '9') { 298 return CType.ONE_DIGIT; 299 } 300 return CType.TWO_DIGITS; 301 } 302 chooseCode(CharSequence value, int start, int oldCode)303 private static int chooseCode(CharSequence value, int start, int oldCode) { 304 CType lookahead = findCType(value, start); 305 if (lookahead == CType.ONE_DIGIT) { 306 if (oldCode == CODE_CODE_A) { 307 return CODE_CODE_A; 308 } 309 return CODE_CODE_B; 310 } 311 if (lookahead == CType.UNCODABLE) { 312 if (start < value.length()) { 313 char c = value.charAt(start); 314 if (c < ' ' || (oldCode == CODE_CODE_A && (c < '`' || (c >= ESCAPE_FNC_1 && c <= ESCAPE_FNC_4)))) { 315 // can continue in code A, encodes ASCII 0 to 95 or FNC1 to FNC4 316 return CODE_CODE_A; 317 } 318 } 319 return CODE_CODE_B; // no choice 320 } 321 if (oldCode == CODE_CODE_A && lookahead == CType.FNC_1) { 322 return CODE_CODE_A; 323 } 324 if (oldCode == CODE_CODE_C) { // can continue in code C 325 return CODE_CODE_C; 326 } 327 if (oldCode == CODE_CODE_B) { 328 if (lookahead == CType.FNC_1) { 329 return CODE_CODE_B; // can continue in code B 330 } 331 // Seen two consecutive digits, see what follows 332 lookahead = findCType(value, start + 2); 333 if (lookahead == CType.UNCODABLE || lookahead == CType.ONE_DIGIT) { 334 return CODE_CODE_B; // not worth switching now 335 } 336 if (lookahead == CType.FNC_1) { // two digits, then FNC_1... 337 lookahead = findCType(value, start + 3); 338 if (lookahead == CType.TWO_DIGITS) { // then two more digits, switch 339 return CODE_CODE_C; 340 } else { 341 return CODE_CODE_B; // otherwise not worth switching 342 } 343 } 344 // At this point, there are at least 4 consecutive digits. 345 // Look ahead to choose whether to switch now or on the next round. 346 int index = start + 4; 347 while ((lookahead = findCType(value, index)) == CType.TWO_DIGITS) { 348 index += 2; 349 } 350 if (lookahead == CType.ONE_DIGIT) { // odd number of digits, switch later 351 return CODE_CODE_B; 352 } 353 return CODE_CODE_C; // even number of digits, switch now 354 } 355 // Here oldCode == 0, which means we are choosing the initial code 356 if (lookahead == CType.FNC_1) { // ignore FNC_1 357 lookahead = findCType(value, start + 1); 358 } 359 if (lookahead == CType.TWO_DIGITS) { // at least two digits, start in code C 360 return CODE_CODE_C; 361 } 362 return CODE_CODE_B; 363 } 364 365 /** 366 * Encodes minimally using Divide-And-Conquer with Memoization 367 **/ 368 private static final class MinimalEncoder { 369 370 private enum Charset { A, B, C, NONE } 371 private enum Latch { A, B, C, SHIFT, NONE } 372 373 static final String A = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\u0000\u0001\u0002" + 374 "\u0003\u0004\u0005\u0006\u0007\u0008\u0009\n\u000B\u000C\r\u000E\u000F\u0010\u0011" + 375 "\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F" + 376 "\u00FF"; 377 static final String B = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqr" + 378 "stuvwxyz{|}~\u007F\u00FF"; 379 380 private static final int CODE_SHIFT = 98; 381 382 private int[][] memoizedCost; 383 private Latch[][] minPath; 384 encode(String contents)385 private boolean[] encode(String contents) { 386 memoizedCost = new int[4][contents.length()]; 387 minPath = new Latch[4][contents.length()]; 388 389 encode(contents, Charset.NONE, 0); 390 391 Collection<int[]> patterns = new ArrayList<>(); 392 int[] checkSum = new int[] {0}; 393 int[] checkWeight = new int[] {1}; 394 int length = contents.length(); 395 Charset charset = Charset.NONE; 396 for (int i = 0; i < length; i++) { 397 Latch latch = minPath[charset.ordinal()][i]; 398 switch (latch) { 399 case A: 400 charset = Charset.A; 401 addPattern(patterns, i == 0 ? CODE_START_A : CODE_CODE_A, checkSum, checkWeight, i); 402 break; 403 case B: 404 charset = Charset.B; 405 addPattern(patterns, i == 0 ? CODE_START_B : CODE_CODE_B, checkSum, checkWeight, i); 406 break; 407 case C: 408 charset = Charset.C; 409 addPattern(patterns, i == 0 ? CODE_START_C : CODE_CODE_C, checkSum, checkWeight, i); 410 break; 411 case SHIFT: 412 addPattern(patterns, CODE_SHIFT, checkSum, checkWeight, i); 413 break; 414 } 415 if (charset == Charset.C) { 416 if (contents.charAt(i) == ESCAPE_FNC_1) { 417 addPattern(patterns, CODE_FNC_1, checkSum, checkWeight, i); 418 } else { 419 addPattern(patterns, Integer.parseInt(contents.substring(i, i + 2)), checkSum, checkWeight, i); 420 assert i + 1 < length; //the algorithm never leads to a single trailing digit in character set C 421 if (i + 1 < length) { 422 i++; 423 } 424 } 425 } else { // charset A or B 426 int patternIndex; 427 switch (contents.charAt(i)) { 428 case ESCAPE_FNC_1: 429 patternIndex = CODE_FNC_1; 430 break; 431 case ESCAPE_FNC_2: 432 patternIndex = CODE_FNC_2; 433 break; 434 case ESCAPE_FNC_3: 435 patternIndex = CODE_FNC_3; 436 break; 437 case ESCAPE_FNC_4: 438 if (charset == Charset.A && latch != Latch.SHIFT || 439 charset == Charset.B && latch == Latch.SHIFT) { 440 patternIndex = CODE_FNC_4_A; 441 } else { 442 patternIndex = CODE_FNC_4_B; 443 } 444 break; 445 default: 446 patternIndex = contents.charAt(i) - ' '; 447 } 448 if ((charset == Charset.A && latch != Latch.SHIFT || 449 charset == Charset.B && latch == Latch.SHIFT) && 450 patternIndex < 0) { 451 patternIndex += '`'; 452 } 453 addPattern(patterns, patternIndex, checkSum, checkWeight, i); 454 } 455 } 456 memoizedCost = null; 457 minPath = null; 458 return produceResult(patterns, checkSum[0]); 459 } 460 461 private static void addPattern(Collection<int[]> patterns, 462 int patternIndex, 463 int[] checkSum, 464 int[] checkWeight, 465 int position) { 466 patterns.add(Code128Reader.CODE_PATTERNS[patternIndex]); 467 if (position != 0) { 468 checkWeight[0]++; 469 } 470 checkSum[0] += patternIndex * checkWeight[0]; 471 } 472 473 private static boolean isDigit(char c) { 474 return c >= '0' && c <= '9'; 475 } 476 477 private boolean canEncode(CharSequence contents, Charset charset,int position) { 478 char c = contents.charAt(position); 479 switch (charset) { 480 case A: return c == ESCAPE_FNC_1 || 481 c == ESCAPE_FNC_2 || 482 c == ESCAPE_FNC_3 || 483 c == ESCAPE_FNC_4 || 484 A.indexOf(c) >= 0; 485 case B: return c == ESCAPE_FNC_1 || 486 c == ESCAPE_FNC_2 || 487 c == ESCAPE_FNC_3 || 488 c == ESCAPE_FNC_4 || 489 B.indexOf(c) >= 0; 490 case C: return c == ESCAPE_FNC_1 || 491 (position + 1 < contents.length() && 492 isDigit(c) && 493 isDigit(contents.charAt(position + 1))); 494 default: return false; 495 } 496 } 497 498 /** 499 * Encode the string starting at position position starting with the character set charset 500 **/ 501 private int encode(CharSequence contents, Charset charset, int position) { 502 assert position < contents.length(); 503 int mCost = memoizedCost[charset.ordinal()][position]; 504 if (mCost > 0) { 505 return mCost; 506 } 507 508 int minCost = Integer.MAX_VALUE; 509 Latch minLatch = Latch.NONE; 510 boolean atEnd = position + 1 >= contents.length(); 511 512 Charset[] sets = new Charset[] { Charset.A, Charset.B }; 513 for (int i = 0; i <= 1; i++) { 514 if (canEncode(contents, sets[i], position)) { 515 int cost = 1; 516 Latch latch = Latch.NONE; 517 if (charset != sets[i]) { 518 cost++; 519 latch = Latch.valueOf(sets[i].toString()); 520 } 521 if (!atEnd) { 522 cost += encode(contents, sets[i], position + 1); 523 } 524 if (cost < minCost) { 525 minCost = cost; 526 minLatch = latch; 527 } 528 cost = 1; 529 if (charset == sets[(i + 1) % 2]) { 530 cost++; 531 latch = Latch.SHIFT; 532 if (!atEnd) { 533 cost += encode(contents, charset, position + 1); 534 } 535 if (cost < minCost) { 536 minCost = cost; 537 minLatch = latch; 538 } 539 } 540 } 541 } 542 if (canEncode(contents, Charset.C, position)) { 543 int cost = 1; 544 Latch latch = Latch.NONE; 545 if (charset != Charset.C) { 546 cost++; 547 latch = Latch.C; 548 } 549 int advance = contents.charAt(position) == ESCAPE_FNC_1 ? 1 : 2; 550 if (position + advance < contents.length()) { 551 cost += encode(contents, Charset.C, position + advance); 552 } 553 if (cost < minCost) { 554 minCost = cost; 555 minLatch = latch; 556 } 557 } 558 if (minCost == Integer.MAX_VALUE) { 559 throw new IllegalArgumentException("Bad character in input: ASCII value=" + (int) contents.charAt(position)); 560 } 561 memoizedCost[charset.ordinal()][position] = minCost; 562 minPath[charset.ordinal()][position] = minLatch; 563 return minCost; 564 } 565 } 566 } 567