/* * Copyright 2014 Robin Stuart * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package uk.org.okapibarcode.backend; import static uk.org.okapibarcode.backend.Ean.calcDigit; import static uk.org.okapibarcode.backend.Ean.validateAndPad; import static uk.org.okapibarcode.backend.HumanReadableLocation.BOTTOM; import static uk.org.okapibarcode.backend.HumanReadableLocation.NONE; import static uk.org.okapibarcode.backend.HumanReadableLocation.TOP; import java.util.Arrays; import uk.org.okapibarcode.graphics.Rectangle; import uk.org.okapibarcode.graphics.TextAlignment; import uk.org.okapibarcode.graphics.TextBox; /** *

Implements UPC bar code symbology according to BS EN 797:1996. * *

UPC-A requires an 11 digit article number. The check digit is calculated. * UPC-E is a zero-compressed version of UPC-A developed for smaller packages. * The code requires a 6 digit article number (digits 0-9). The check digit * is calculated. Also supports Number System 1 encoding by entering a 7-digit * article number stating with the digit 1. * *

EAN-2 and EAN-5 add-on symbols can be added using the '+' character followed * by the add-on data. * * @author Robert Elliott */ public class Upc extends Symbol { /** The different UPC barcode variants available to encode. */ public enum Mode { /** UPC-A */ UPCA, /** UPC-E */ UPCE } private static final String[] SET_AC = { "3211", "2221", "2122", "1411", "1132", "1231", "1114", "1312", "1213", "3112" }; private static final String[] SET_B = { "1123", "1222", "2212", "1141", "2311", "1321", "4111", "2131", "3121", "2113" }; /* Number set for UPC-E symbol (EN Table 4) */ private static final String[] UPC_PARITY_0 = { "BBBAAA", "BBABAA", "BBAABA", "BBAAAB", "BABBAA", "BAABBA", "BAAABB", "BABABA", "BABAAB", "BAABAB" }; /* Not covered by BS EN 797 */ private static final String[] UPC_PARITY_1 = { "AAABBB", "AABABB", "AABBAB", "AABBBA", "ABAABB", "ABBAAB", "ABBBAA", "ABABAB", "ABABBA", "ABBABA" }; private Mode mode; private boolean showCheckDigit = true; private int guardPatternExtraHeight = 5; private boolean linkageFlag; private EanUpcAddOn addOn; /** * Creates a new instance, using mode {@link Mode#UPCA}. */ public Upc() { this(Mode.UPCA); } /** * Creates a new instance, using the specified mode. * * @param mode the UPC mode (UPC-A or UPC-E) */ public Upc(Mode mode) { this.mode = mode; this.humanReadableAlignment = TextAlignment.JUSTIFY; } /** * Sets the UPC mode (UPC-A or UPC-E). The default is UPC-A. * * @param mode the UPC mode (UPC-A or UPC-E) */ public void setMode(Mode mode) { this.mode = mode; } /** * Returns the UPC mode (UPC-A or UPC-E). * * @return the UPC mode (UPC-A or UPC-E) */ public Mode getMode() { return mode; } /** * Sets whether or not to show the check digit in the human-readable text. * * @param showCheckDigit whether or not to show the check digit in the human-readable text */ public void setShowCheckDigit(boolean showCheckDigit) { this.showCheckDigit = showCheckDigit; } /** * Returns whether or not to show the check digit in the human-readable text. * * @return whether or not to show the check digit in the human-readable text */ public boolean getShowCheckDigit() { return showCheckDigit; } /** * Sets the extra height used for the guard patterns. The default value is 5. * * @param guardPatternExtraHeight the extra height used for the guard patterns */ public void setGuardPatternExtraHeight(int guardPatternExtraHeight) { this.guardPatternExtraHeight = guardPatternExtraHeight; } /** * Returns the extra height used for the guard patterns. * * @return the extra height used for the guard patterns */ public int getGuardPatternExtraHeight() { return guardPatternExtraHeight; } /** * Returns the UPC add-on content which was encoded, if any. Only available after {@link #setContent(String)} is called. * * @return the UPC add-on content which was encoded, if any */ public String getAddOnContent() { return addOn != null ? addOn.getContent() : null; } /** * Sets the linkage flag. If set to true, this symbol is part of a composite symbol. * * @param linkageFlag the linkage flag */ protected void setLinkageFlag(boolean linkageFlag) { this.linkageFlag = linkageFlag; } @Override protected void encode() { separateContent(); if (content.isEmpty() && !emptyContentAllowed) { throw new OkapiInputException("Missing UPC data"); } if (mode == Mode.UPCA) { upca(); } else { upce(); } } private void separateContent() { int splitPoint = content.indexOf('+'); if (splitPoint == -1) { // there is no add-on data addOn = null; } else if (splitPoint == content.length() - 1) { // we found the add-on separator, but no add-on data throw new OkapiInputException("Invalid add-on data"); } else { // there is a '+' in the input data, use an add-on EAN2 or EAN5 addOn = new EanUpcAddOn(); addOn.font = this.font; addOn.fontName = this.fontName; addOn.fontSize = this.fontSize; addOn.humanReadableLocation = (this.humanReadableLocation == NONE ? NONE : TOP); addOn.moduleWidth = this.moduleWidth; addOn.default_height = this.default_height + this.guardPatternExtraHeight - 8; addOn.setContent(content.substring(splitPoint + 1)); content = content.substring(0, splitPoint); } } private void upca() { content = validateAndPad(content, 11); char check = calcDigit(content); infoLine("Check Digit: " + check); String hrt = content + check; StringBuilder dest = new StringBuilder("111"); for (int i = 0; i < 12; i++) { if (i == 6) { dest.append("11111"); } dest.append(SET_AC[hrt.charAt(i) - '0']); } dest.append("111"); readable = hrt; pattern = new String[] { dest.toString() }; row_count = 1; row_height = new int[] { -1 }; } private void upce() { content = validateAndPad(content, 7); String expanded = expandToEquivalentUpcA(content, true); infoLine("UPC-A Equivalent: " + expanded); char check = calcDigit(expanded); infoLine("Check Digit: " + check); String hrt = content + check; int numberSystem = getNumberSystem(content); String[] parityArray = (numberSystem == 1 ? UPC_PARITY_1 : UPC_PARITY_0); String parity = parityArray[check - '0']; StringBuilder dest = new StringBuilder("111"); for (int i = 0; i < 6; i++) { if (parity.charAt(i) == 'A') { dest.append(SET_AC[content.charAt(i + 1) - '0']); } else { // B dest.append(SET_B[content.charAt(i + 1) - '0']); } } dest.append("111111"); readable = hrt; pattern = new String[] { dest.toString() }; row_count = 1; row_height = new int[] { -1 }; } /** * Expands the zero-compressed UPC-E code to make a UPC-A equivalent (EN Table 5). * * @param content the UPC-E code to expand * @param validate whether or not to validate the input * @return the UPC-A equivalent of the specified UPC-E code */ protected String expandToEquivalentUpcA(String content, boolean validate) { char[] upce = content.toCharArray(); char[] upca = new char[11]; Arrays.fill(upca, '0'); upca[0] = upce[0]; upca[1] = upce[1]; upca[2] = upce[2]; char emode = upce[6]; switch (emode) { case '0': case '1': case '2': upca[3] = emode; upca[8] = upce[3]; upca[9] = upce[4]; upca[10] = upce[5]; break; case '3': upca[3] = upce[3]; upca[9] = upce[4]; upca[10] = upce[5]; if (validate && (upce[3] == '0' || upce[3] == '1' || upce[3] == '2')) { /* Note 1 - "X3 shall not be equal to 0, 1 or 2" */ throw new OkapiInputException("Invalid UPC-E data"); } break; case '4': upca[3] = upce[3]; upca[4] = upce[4]; upca[10] = upce[5]; if (validate && upce[4] == '0') { /* Note 2 - "X4 shall not be equal to 0" */ throw new OkapiInputException("Invalid UPC-E data"); } break; default: upca[3] = upce[3]; upca[4] = upce[4]; upca[5] = upce[5]; upca[10] = emode; if (validate && upce[5] == '0') { /* Note 3 - "X5 shall not be equal to 0" */ throw new OkapiInputException("Invalid UPC-E data"); } break; } return new String(upca); } /** Two number systems can be used: system 0 and system 1. */ private static int getNumberSystem(String content) { switch (content.charAt(0)) { case '0': return 0; case '1': return 1; default: throw new OkapiInputException("Invalid input data"); } } @Override protected void plotSymbol() { resetPlotElements(); int xBlock; int x, y, w, h; boolean black = true; int compositeOffset = (linkageFlag ? 6 : 0); // space for composite separator above int hrtOffset = (humanReadableLocation == TOP ? getTheoreticalHumanReadableHeight() : 0); // space for HRT above x = 0; /* Draw the bars in the symbology */ for (xBlock = 0; xBlock < pattern[0].length(); xBlock++) { w = pattern[0].charAt(xBlock) - '0'; if (black) { y = 0; h = default_height; /* Add extension to guide bars */ if (mode == Mode.UPCA) { if (x < 10 || x > 84 || (x > 45 && x < 49)) { h += guardPatternExtraHeight; } if (linkageFlag && (x == 0 || x == 94)) { h += 2; y -= 2; } } else { if (x < 4 || x > 45) { h += guardPatternExtraHeight; } if (linkageFlag && (x == 0 || x == 50)) { h += 2; y -= 2; } } Rectangle rect = new Rectangle(scale(x), y + compositeOffset + hrtOffset, scale(w), h); rectangles.add(rect); symbol_width = Math.max(symbol_width, (int) (rect.x + rect.width)); symbol_height = Math.max(symbol_height, (int) rect.height); } black = !black; x += w; } /* Add separator for composite symbology, if necessary */ if (linkageFlag) { if (mode == Mode.UPCA) { rectangles.add(new Rectangle(scale(0), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(94), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(-1), 2, scale(1), 2)); rectangles.add(new Rectangle(scale(95), 2, scale(1), 2)); } else { // UPCE rectangles.add(new Rectangle(scale(0), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(50), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(-1), 2, scale(1), 2)); rectangles.add(new Rectangle(scale(51), 2, scale(1), 2)); } symbol_height += 4; } /* Now add the text */ if (humanReadableLocation == BOTTOM) { symbol_height -= guardPatternExtraHeight; double baseline = symbol_height + fontSize; if (mode == Mode.UPCA) { texts.add(new TextBox(scale(-9), baseline, scale(4), readable.substring(0, 1), TextAlignment.RIGHT)); texts.add(new TextBox(scale(12), baseline, scale(32), readable.substring(1, 6), humanReadableAlignment)); texts.add(new TextBox(scale(51), baseline, scale(32), readable.substring(6, 11), humanReadableAlignment)); if (showCheckDigit) { texts.add(new TextBox(scale(97), baseline, scale(4), readable.substring(11, 12), TextAlignment.LEFT)); } } else { // UPCE texts.add(new TextBox(scale(-9), baseline, scale(4), readable.substring(0, 1), TextAlignment.RIGHT)); texts.add(new TextBox(scale(5), baseline, scale(39), readable.substring(1, 7), humanReadableAlignment)); if (showCheckDigit) { texts.add(new TextBox(scale(53), baseline, scale(4), readable.substring(7, 8), TextAlignment.LEFT)); } } } else if (humanReadableLocation == TOP) { double baseline = fontSize; int width = (mode == Mode.UPCA ? 94 : 50); texts.add(new TextBox(scale(0), baseline, scale(width), readable, humanReadableAlignment)); } /* Now add the add-on symbol, if necessary */ if (addOn != null) { int gap = 9; int baseX = symbol_width + scale(gap); Rectangle r1 = rectangles.get(0); Rectangle ar1 = addOn.rectangles.get(0); int baseY = (int) (r1.y + r1.height - ar1.y - ar1.height); for (TextBox t : addOn.getTexts()) { texts.add(new TextBox(baseX + t.x, baseY + t.y, t.width, t.text, t.alignment)); } for (Rectangle r : addOn.getRectangles()) { rectangles.add(new Rectangle(baseX + r.x, baseY + r.y, r.width, r.height)); } symbol_width += scale(gap) + addOn.symbol_width; pattern[0] = pattern[0] + gap + addOn.pattern[0]; } } /** Scales the specified width or x-dimension according to the current module width. */ private int scale(int w) { return moduleWidth * w; } }