/* * 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.HumanReadableLocation.BOTTOM; import static uk.org.okapibarcode.backend.HumanReadableLocation.NONE; import static uk.org.okapibarcode.backend.HumanReadableLocation.TOP; import uk.org.okapibarcode.graphics.Rectangle; import uk.org.okapibarcode.graphics.TextAlignment; import uk.org.okapibarcode.graphics.TextBox; /** *

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

European Article Number data can be encoded in EAN-8 or EAN-13 format requiring a 7-digit * or 12-digit input respectively. EAN-13 numbers map to Global Trade Identification Numbers (GTIN) * whereas EAN-8 symbols are generally for internal use only. Check digit is calculated and should not * be in input data. Leading zeroes are added as required. * *

Add-on content can be appended to the main symbol content by adding a {@code '+'} character, * followed by the add-on content (up to 5 digits). * * @author Robert Elliott */ public class Ean extends Symbol { /** The different EAN barcode variants available to encode. */ public enum Mode { /** EAN-8 */ EAN8, /** EAN-13 */ EAN13 } private static final String[] EAN13_PARITY = { "AAAAAA", "AABABB", "AABBAB", "AABBBA", "ABAABB", "ABBAAB", "ABBBAA", "ABABAB", "ABABBA", "ABBABA" }; private static final String[] EAN_SET_A = { "3211", "2221", "2122", "1411", "1132", "1231", "1114", "1312", "1213", "3112" }; private static final String[] EAN_SET_B = { "1123", "1222", "2212", "1141", "2311", "1321", "4111", "2131", "3121", "2113" }; private Mode mode; private int guardPatternExtraHeight = 5; private boolean linkageFlag; private EanUpcAddOn addOn; /** * Creates a new instance, using mode {@link Mode#EAN13}. */ public Ean() { this(Mode.EAN13); } /** * Creates a new instance, using the specified mode. * * @param mode the EAN mode (EAN-8 or EAN-13) */ public Ean(Mode mode) { this.mode = mode; this.humanReadableAlignment = TextAlignment.JUSTIFY; } /** * Sets the EAN mode (EAN-8 or EAN-13). The default is EAN-13. * * @param mode the EAN mode (EAN-8 or EAN-13) */ public void setMode(Mode mode) { this.mode = mode; } /** * Returns the EAN mode (EAN-8 or EAN-13). * * @return the EAN mode (EAN-8 or EAN-13) */ public Mode getMode() { return mode; } /** * 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 EAN add-on content which was encoded, if any. Only available after {@link #setContent(String)} is called. * * @return the EAN 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 EAN data"); } if (mode == Mode.EAN8) { ean8(); } else { ean13(); } } 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 ean13() { content = validateAndPad(content, 12); char check = calcDigit(content); infoLine("Check Digit: " + check); String hrt = content + check; char parityChar = hrt.charAt(0); String parity = EAN13_PARITY[parityChar - '0']; infoLine("Parity Digit: " + parityChar); StringBuilder dest = new StringBuilder("111"); for (int i = 1; i < 13; i++) { if (i == 7) { dest.append("11111"); } if (i <= 6) { if (parity.charAt(i - 1) == 'B') { dest.append(EAN_SET_B[hrt.charAt(i) - '0']); } else { dest.append(EAN_SET_A[hrt.charAt(i) - '0']); } } else { dest.append(EAN_SET_A[hrt.charAt(i) - '0']); } } dest.append("111"); readable = hrt; pattern = new String[] { dest.toString() }; row_count = 1; row_height = new int[] { -1 }; } private void ean8() { content = validateAndPad(content, 7); char check = calcDigit(content); infoLine("Check Digit: " + check); String hrt = content + check; StringBuilder dest = new StringBuilder("111"); for (int i = 0; i < 8; i++) { if (i == 4) { dest.append("11111"); } dest.append(EAN_SET_A[hrt.charAt(i) - '0']); } dest.append("111"); readable = hrt; pattern = new String[] { dest.toString() }; row_count = 1; row_height = new int[] { -1 }; } protected static String validateAndPad(String s, int targetLength) { if (!s.matches("[0-9]*")) { throw OkapiInputException.invalidCharactersInInput(); } if (s.length() > targetLength) { throw OkapiInputException.inputTooLong(); } if (s.length() < targetLength) { for (int i = s.length(); i < targetLength; i++) { s = '0' + s; } } return s; } protected static char calcDigit(String s) { int count = 0; int p = 0; for (int i = s.length() - 1; i >= 0; i--) { int c = Character.getNumericValue(s.charAt(i)); if (p % 2 == 0) { c = c * 3; } count += c; p++; } int cdigit = 10 - (count % 10); if (cdigit == 10) { cdigit = 0; } return (char) (cdigit + '0'); } @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.EAN13) { if (x < 3 || x > 91 || (x > 45 && x < 49)) { h += guardPatternExtraHeight; } if (linkageFlag && (x == 0 || x == 94)) { h += 2; y -= 2; } } else { if (x < 3 || x > 62 || (x > 30 && x < 35)) { h += guardPatternExtraHeight; } if (linkageFlag && (x == 0 || x == 66)) { 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.EAN13) { 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 { // EAN8 rectangles.add(new Rectangle(scale(0), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(66), 0, scale(1), 2)); rectangles.add(new Rectangle(scale(-1), 2, scale(1), 2)); rectangles.add(new Rectangle(scale(67), 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.EAN13) { 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)); texts.add(new TextBox(scale(51), baseline, scale(39), readable.substring(7, 13), humanReadableAlignment)); } else { // EAN8 texts.add(new TextBox(scale(5), baseline, scale(25), readable.substring(0, 4), humanReadableAlignment)); texts.add(new TextBox(scale(37), baseline, scale(25), readable.substring(4, 8), humanReadableAlignment)); } } else if (humanReadableLocation == TOP) { double baseline = fontSize; int width = (mode == Mode.EAN13 ? 94 : 66); 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; } }