/* * 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;
}
}